Spring的学习之路(必看)

Spring的基本使用

第一章 Spring简介

Spring官网链接:官网

Spring的五大功能

功能模块功能介绍
Core Container核心容器,在 Spring 环境下使用任何功能都必须基于 IOC 容器。
AOP&Aspects面向切面编程
Testing提供了对 junit 或 TestNG 测试框架的整合。
Data Access/Integration提供了对数据访问/集成的功能。
Spring MVC提供了面向Web应用程序的集成功能。

Spring的核心就是控制反转(IOC)依赖注入(DI)面向切面编程(AOP),之后会给大家介绍这些概念。

第二章 IOC容器

1.1 IOC容器概念

1.1.1 程序中的容器

  1. 数组
  2. 集合List、Set、Map
  3. Servlet容器能够管理 Servlet、Filter、Listener

Servlet的生命周期

名称时机次数
创建对象默认情况:接收到第一次请求修改启动顺序后:Web应用启动过程中一次
初始化创建对象之后一次
处理请求接收到请求多次
销毁Web应用卸载之前一次

Filter生命周期

名称时机次数
创建对象Web应用启动时一次
初始化创建对象之后一次
拦截请求接收到匹配的请求多次
销毁Web应用卸载之前一次

1.1.2 IOC思想

IOC概念:Inversion of Control,中文意思是控制反转。它是软件设计模式中的一种设计原则和思想。

获取资源的传统方式
在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,因此开发人员需要知道在具体容器中特定资源的获取方式。

控制反转的方式获取资源
将你设计好的对象交给容器控制,而不是显式的用代码进行对象的创建。
把创建和查找依赖对象的控制权交给IOC容器,由IOC容器进行注入、组合对象。
这也就是说容器主动的将资源推送给需要的组件,而开发人员是不需要知道容器是如何创建资源的,只需要提供接收资源的方式。

使用IOC的好处

  1. 资源集中管理,实现资源的可配置和易管理。
  2. 降低了资源之间的依赖程度,即松耦合
  3. 减少对象的创建和内存消耗。
  4. 使得程序的整个体系结构可维护、灵活性、扩展性变高。

DI
DI:Dependency Injection,中文意思是依赖注入。他也是IOC的另外一个说法。就是组件之间依赖关系由容器在运行期决定。组件的创建都由容器来创建,再由容器来决定各组件之间的依赖关系。各个组件不再自己决定依赖的其他对象。换句话说,就是组件运行所依赖的对象只能是注入进来的对象,注入哪个对象,我就依赖哪个!

1.2 基于XML管理Bean

1.2.1 创建bean

由Spring的IOC容器来创建类的对象。

步骤:
①创建一个Maven Module
②在pom.xml中添加依赖

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.10</version>
</dependency>

spring-context的依赖包

在这里插入图片描述
③创建组件类

public class Computer {
    public void work(){
        System.out.println("==========电脑启动=======");
    }
}

④创建Spring的配置文件
注意:这个spring配置文件的名字可以自定义

在这里插入图片描述
在这里插入图片描述

⑤编写spring.xml文件

bean标签:通过配置bean标签告诉IOC容器需要创建对象的组件是什么。
name属性:bean的唯一标识。
id属性:bean的唯一标识。
class属性:组件类的全类名。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean name="computer" class="com.zhang.Computer">
    </bean>
</beans>

⑥测试加载

public class Test {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring.xml");
        Computer computer = applicationContext.getBean("computer", Computer.class);
        computer.work();
    }
}

1.2.2 获取bean

有两种方式获取bean。

  1. 根据name属性或id属性获取
  2. 根据类型获取
1.2.2.1 根据name属性或id属性获取

注意:name属性和id属性的值必须是唯一的,不能重复
在spring.xml中bean上不管是name属性还是id属性,只需要在getBean时写上他们的值就可以获取到了。

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring.xml");
// 如果不命名第二个参数,获取的就是Object对象,需要强转为Computer类
// Computer computer = (Computer)applicationContext.getBean("computer");
Computer computer = applicationContext.getBean("computer", Computer.class);
computer.work();
1.2.2.2 根据类型获取

注意: 如果通过类型获取,在xml文件中只能有一个该类型的bean;如果有多个该类型的bean,则会报 org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type ‘com.zhang.Computer’ available: expected single matching bean but found 2: computer,computer1 这个错误。

 ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring.xml");
 Computer computer = applicationContext.getBean(Computer.class);
 computer.work();

1.2.3 属性注入

实体类如下:

public class CPU {
    private String name;
    
	public CPU(){}

    public CPU(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

public class Computer {

    private String name;
    private CPU cpu;

  	public Computer(){}
    
    public Computer(String name, CPU cpu) {
        this.name = name;
        this.cpu = cpu;
    }

    public CPU getCpu() {
        return cpu;
    }

    public void setCpu(CPU cpu) {
        this.cpu = cpu;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
    
    public void work(){
        System.out.println("==========电脑启动=======");
    }
}
1.2.3.1 setter方式
1.2.3.1.1 < property > 标签注入

property 标签:通过实体类的setXxx() 方法给实体类对象设置属性;
name属性指定属性名(这个属性名是getXxx()、setXxx()方法定义的,和成员变量无关,与其方法的参数有关);
value属性指定属性值
ref属性通过bean的id引用另一个bean

<bean id="cpu" class="com.zhang.pojo.CPU">
        <property name="name" value="Intel"/>
</bean>
    
<bean id="computer" class="com.zhang.pojo.Computer">
    <property name="name" value="惠普"/>
    <!-- 这个ref注入时,idea有提示,并且ref只能匹配setXxx()方法中参数的相对应的实体类类型-->
    <property name="cpu" ref="cpu"/>
</bean>

注意:ref和value属性的区别:当我们使用value属性时,Spring会直接给该属性赋值,Spring不会把这个属性作为一个bean的id,也不会去找相应的bean来赋值;而ref属性会找自己相对应的bean(类型),即引用这个对象

1.2.3.1.2 <p:prop> 命名空间注入

导入命名空间
注意:命名空间你可以先写p:,然后使用Alt+Enter,idea帮你自动导入命名空间。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
</beans>

如上面使用 property 标签注入的方式,我们可以改成 <p:prop> 命名空间注入的形式。
注意: p命名空间后边跟着属性名相当于注入,而后边跟着属性名-ref相当于注入实体类

<bean id="cpu" class="com.zhang.pojo.CPU" p:name="Intel"/>

<bean id="computer" class="com.zhang.pojo.Computer" p:name="惠普" p:cpu-ref="cpu"/>
1.2.3.2 构造器方式
1.2.3.2.1 位置index注入

index属性指定参数所在位置的索引(从0开始)

<bean id="cpu" class="com.zhang.pojo.CPU">
        <constructor-arg index="0" value="Inter"/>
</bean>

<bean id="computer" class="com.zhang.pojo.Computer">
        <constructor-arg index="0" value="惠普"/>
        <constructor-arg index="1" ref="cpu"/>
</bean>
1.2.2.3.2 命名参数注入

name属性指定属性名
value属性指定的属性值
ref属性通过bean的id引用另一个bean

 <bean id="cpu" class="com.zhang.pojo.CPU">
        <constructor-arg name="name" value="Inter"/>
 </bean>

 <bean id="computer" class="com.zhang.pojo.Computer">
 		<!--这个name属性可以省略-->
        <constructor-arg name="name" value="惠普"/>
        <constructor-arg name="cpu" ref="cpu"/>
 </bean>
1.2.3.3 自动装配方式

使用bean标签的autowire属性设置自动装配效果;

1.2.3.3.1 根据类型进行装配

若autowire属性的值为byType,则表示根据类型进行装配,注意:上下文默认情况下最多只有一个类型,如果有多个类型,通过设置primary=“true” ,则此bean就是优先注入的;如果给其中多余的bean设置autowire-candidate=“false” 属性,则就不会作为自动装配的候选。

	<bean id="cpu1" class="com.zhang.pojo.CPU" autowire-candidate="false">
        <property name="name" value="Intel"/>
    </bean>

    <bean id="cpu2" class="com.zhang.pojo.CPU" primary="true">
        <property name="name" value="AMD"/>
    </bean>

    <bean id="computer" class="com.zhang.pojo.Computer" autowire="byType">
    </bean>
1.2.3.3.2 根据属性名进行装配

若autowire属性的值为byName,则表示根据bean的id进行匹配。而bean的id是根据需要装配组件的属性的属性名来确定的;

	 <bean id="cpu" class="com.zhang.pojo.CPU">
        <property name="name" value="AMD"/>
    </bean>

    <bean id="computer" class="com.zhang.pojo.Computer" autowire="byName">
    </bean>
1.2.3.3.3 根据构造器进行装配

若autowire属性的值为constructor,则表示通过构造器来匹配
首先匹配对应参数,名字,类型
如果有多个类型参数时,优先名字匹配,如果名字匹配成功则正常注入,否则不执行有参构造方法,不能注入

	<bean id="cpu1" class="com.zhang.pojo.CPU">
        <property name="name" value="Intel"/>
    </bean>
    
	 <bean id="cpu" class="com.zhang.pojo.CPU">
        <property name="name" value="AMD"/>
    </bean>

    <bean id="computer" class="com.zhang.pojo.Computer" autowire="constructor">
    </bean>
1.2.3.4 通过内部bean方式
	<bean id="computer" class="com.zhang.pojo.Computer">
        <property name="name" value="惠普"/>
        <property name="cpu">
        <!-- 在一个 bean 中再声明一个 bean 就是内部 bean -->
        <!-- 内部 bean 可以直接用于给属性赋值,可以省略 id 和 name属性 -->
        	<bean class="com.zhang.pojo.CPU">
            	<property name="name" value="Intel"/>
        	</bean>
        </property>
    </bean>

1.2.4 特殊类型的注入以及特殊值处理

实体类如下

public class CollectionBean {
    private List<String> list;
    private Set<String> set;
    private String[] arr;
    private Map<String,String> map;
    private Properties prop;
    
    public CollectionBean(){}
    public CollectionBean(List<String> list,Set<String> set){
        this.list = list;
        this.set = set;
    }

    public List<String> getList() {
        return list;
    }

    public void setList(List<String> list) {
        this.list = list;
    }

    public Set<String> getSet() {
        return set;
    }

    public void setSet(Set<String> set) {
        this.set = set;
    }

    public String[] getArr() {
        return arr;
    }

    public void setArr(String[] arr) {
        this.arr = arr;
    }

    public Map<String, String> getMap() {
        return map;
    }

    public void setMap(Map<String, String> map) {
        this.map = map;
    }

    public Properties getProp() {
        return prop;
    }

    public void setProp(Properties prop) {
        this.prop = prop;
    }
}
1.2.4.1 集合类型的属性注入

给List、Set、数组、Map、Properties等这些特殊类型进行注入。

	<bean id="collectionBean" class="com.zhang.pojo.CollectionBean">
        <!-- list标签:准备一组集合类型的数据,给集合属性赋值 -->
        <property name="list">
            <list>
                <value>张三</value>
                <value>李四</value>
                <value>王麻子</value>
            </list>
        </property>
        <!-- 使用array标签给数组属性赋值 -->
        <property name="arr">
            <array>
                <value>张三</value>
                <value>李四</value>
                <value>王麻子</value>
            </array>
        </property>
        <!-- 使用map标签给Map类型的属性赋值 -->
        <property name="map">
            <map>
                <entry key="张三" value="123"/>
                <entry key="李四" value="456"/>
                <entry key="王五" value="789"/>
            </map>
        </property>
        <!-- 使用set标签给集合属性赋值,只是附带了去重功能 -->
        <property name="set">
            <set>
                <value>张三</value>
                <value>李四</value>
                <value>王麻子</value>
            </set>
        </property>
        <!-- 可以使用props标签 -->
        <property name="prop">
            <props>
                <prop key="张三">123</prop>
                <prop key="李四">456</prop>
                <prop key="王五">789</prop>
            </props>
        </property>
    </bean>
1.2.4.2 赋null值

null标签:将一个属性值明确设置为null

	<bean id="bean" class="com.zhang.pojo.CollectionBean">
        <constructor-arg index="0">
        	<!-- null标签:将一个属性值明确设置为null -->
            <null/>
        </constructor-arg>
        <constructor-arg index="1">
       		<!-- null标签:将一个属性值明确设置为null -->
            <null/>
        </constructor-arg>
    </bean>
1.2.4.3 赋特殊符号(<、>、>=、<=)

注意: 小于号在XML文档中用来定义标签的开始,不能随便使用,如果需要使用的话,需要使用XML实体来代替或者使用CDATA节

CDATA中的C代表Character,是文本、字符的含义CDATA就表示纯文本数据XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析,因此CDATA节中写什么符号都随意

实体类如下:

public class Expression {
    private String expression;

    public String getExpression() {
        return expression;
    }

    public void setExpression(String expression) {
        this.expression = expression;
    }
}
<bean id="expression" class="com.zhang.pojo.Expression">
        <property name="expression">
            <value><![CDATA[ a < b]]></value>
        </property>
</bean>

1.2.5 级联属性赋值

Spring 级联属性是当两个bean 关联时,从一个bean 给 另一个bean 赋值。

第一种写法:

	<bean id="cpu" class="com.zhang.pojo.CPU">
    </bean>

    <bean id="computer" class="com.zhang.pojo.Computer">
        <property name="name" value="惠普"/>
        <property name="cpu" ref="cpu"/>
        <!-- 对于Computer来说,cpu的name属性就是级联属性 -->
        <property name="cpu.name" value="Inter"/>
    </bean>

第二种写法:这种写法之前也提到过。

	<bean id="cpu" class="com.zhang.pojo.CPU">
        <property name="name" value="Inter"/>
    </bean>

    <bean id="computer" class="com.zhang.pojo.Computer">
        <property name="name" value="惠普"/>
        <property name="cpu" ref="cpu"/>
    </bean>

1.2.6 引入外部属性文件

在pom.xml中加入依赖

		<!-- MySQL驱动 -->
		<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>
         <!-- 数据源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.8</version>
        </dependency>

创建外部属性文件

在这里插入图片描述

jdbc.url=jdbc:mysql://localhost:3306/testdb
jdbc.driver=com.mysql.jdbc.Driver
jdbc.user=root
jdbc.password=123456

在spring.xml文件中引入外部文件,并且使用外部文件。
在这之前,先引入命名空间

xmlns:context="http://www.springframework.org/schema/context"
	<context:property-placeholder location="classpath:jdbc.properties"/>
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="username" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>

测试

		ApplicationContext applicationContext = new ClassPathXmlApplicationContext("/spring.xml");
		DataSource dataSource = applicationContext.getBean("dataSource", DataSource.class);
        try {
            System.out.println(dataSource.getConnection());
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }

1.2.7 继承和抽象

可以通过继承来实现代码的复用,属性注入的复用

创建两个实体类:Person和Student

public class Person {
    private String address;
    private String sex;
    private String school;
    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getSchool() {
        return school;
    }

    public void setSchool(String school) {
        this.school = school;
    }
}

public class Student {   // 本身和Person 没有继承关系
        private String address;
        private String sex;
        private String school;
        private String name;

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    public String getSchool() {
        return school;
    }

    public void setSchool(String school) {
        this.school = school;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

在spring.xml文件中配置

	<!-- 模板,不希望被实例化,设置为抽象的-->
	<bean id="person" class="com.zhang.pojo.Person" abstract="true">
        <property name="address" value="北京"/>
        <property name="school" value="北京邮电大学"/>
        <property name="sex" value=""/>
    </bean>
	<!-- 使用上面bean 中的属性,可以parent="person" 继承过来-->
    <bean id="student" class="com.zhang.pojo.Student" parent="person">
        <property name="name" value="张三"/>
    </bean>

1.2.8 拆分

1.2.8.1 在 xml 中 import 导入其他的 xml

创建a.xml文件
在这里插入图片描述

在a.xml文件中编写bean

<bean id="cpu" class="com.zhang.pojo.CPU">
        <property name="name" value="Inter"/>
</bean>

在spring.xml文件引入a.xml文件,并且使用a.xml文件中的bean。

	<import resource="a.xml"/>
    <bean class="com.zhang.pojo.Computer">
        <property name="cpu" ref="cpu"/>
    </bean>
1.2.8.2 在代码应用中,可以同时加载多个xml文件
 String[] files = {"a.xml","spring.xml"};
 ApplicationContext context = new ClassPathXmlApplicationContext(files);
1.2.8.3 容器之间继承
	<bean name="computer" class="com.zhang.pojo.Computer">
        <property name="cpu">
        	<!--parent:去父容器中查找-->
            <ref parent="cpu"/>
        </property>
    </bean>

1.2.9 生命周期

1.2.9.1 bean的作用域

在Spring中可以通过配置bean标签的scope属性来指定bean的作用域范围,各取值含义参加下表:

取值含义创建对象的时机
singleton在IOC容器中,这个bean的对象始终为单实例,不管多少次getBean,只实例化一次IOC容器初始化时
prototype这个bean在IOC容器中有多个实例,每次getBean时,都会实例化获取bean时
1.2.9.2 bean的生命周期
lazy-init="false" : 立即加载,创建上下文时,立即构造bean 对象
lazy-init="true" : 延时加载,在使用时,获取bean 时,才去实例加载

bean的生命周期清单

  1. bean对象创建(调用无参构造器)
  2. 给bean对象设置属性
  3. bean对象初始化之前操作(由bean的后置处理器负责)
  4. bean对象初始化(需在配置bean时指定初始化方法)
  5. bean对象初始化之后操作(由bean的后置处理器负责)
  6. bean对象就绪可以使用
  7. bean对象销毁(需在配置bean时指定销毁方法)
  8. IOC容器关闭

bean的初始化方法和销毁方法

	public void init(){
        System.out.println("初始化方法");
    }
    
    public void destroy(){
        System.out.println("销毁方法");
    }

使用init-method属性指定初始化方法
使用destroy-method属性指定销毁方法

	<bean id="cpu" class="com.zhang.pojo.CPU" init-method="init" destroy-method="destroy">
        <property name="name" value="Inter"/>
    </bean>

bean的后置处理器

创建后置处理器

// 声明一个自定义的bean后置处理器
// 注意:bean后置处理器不是单独针对某一个bean生效,而是针对IOC容器中所有bean都会执行
public class BeanProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("==== " + beanName + " = " + bean);
        return null;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("--- " + beanName + " = " + bean);
        return null;
    }
}

将bean的后置处理器放入IOC容器

因为bean的后置处理器要放入IOC容器才能生效。

<bean id="beanProcessor" class="com.zhang.BeanProcessor"/>

1.3 基于注解管理Bean

注解的概念:和XML文件一样,注解本身并不能执行,注解本身仅仅只是做一个标记,具体的功能是框架检测到注解标记的位置,然后针对这个位置按照注解标记的功能来执行具体操作。

但是追根本源:所有一切的操作都是由Java代码来完成的,XML和注解只是告诉框架中的Java代码如何执行。

注意: 我们使用注解后,每一个组件必须保证有一个唯一的标识。

1.3.1 标记与扫描

扫描的概念:Spring要通过扫描包,才能知道开发人员在哪标记了注解,然后通过注解进行后面的操作。

1.3.1.1 创建四个常用组件类

使用@Component注解标记普通组件

import org.springframework.stereotype.Component;

@Component
public class CommonComponent {
}

使用@Repository注解标记持久化层组件

import org.springframework.stereotype.Repository;

@Repository
public class UserDaoImpl implements UserDao {

}

使用@Service注解标记业务逻辑组件

import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
}

使用@Controller注解标记控制层组件

import org.springframework.stereotype.Controller;@Controller
public class UserController {
}

上面四个组件之间的对比:

我们可以点进去查看源码,我们会发现:@Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。对于Spring的IOC容器来说,这三个组件本质上没有什么区别,而他们的的作用主要是来提高代码的可读性

1.3.1.2 扫描的使用

最基本扫描的配置

<!-- 配置自动扫描的包 -->
<context:component-scan base-package="com.zhang"/>

在配置扫描包的基础上指定匹配模式

<!-- resource-pattern属性:配置要扫描资源的正则表达式的 -->
<context:component-scan base-package="com.zhang" resource-pattern="**/User*.class"/>

在配置扫描包的基础上指定不需要扫描的包

<context:component-scan base-package="com.zhang">
        <!--
            context:exclude-filter标签:指定一些不需要扫描的包
            type属性:指定根据什么来进行去除。例如:annotation取值是根据注解来排除。
            expression属性:指定全类名,表示过滤器的表达式
        -->
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

在配置扫描包的基础上指定其他需要的规则

	<!-- 仅扫描 = 关闭默认规则 + 追加规则 -->
    <!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
    <context:component-scan base-package="com.zhang" use-default-filters="false">
        <!-- context:include-filter标签: 在配置扫描包的基础上添加其他需要的规则 -->
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>

注意:被组件标记后,默认情况下获取bean,就是类名的首字母小写
如:UserServiceImpl类对应的bean的id或name属性就是 userServiceImpl

当然,我们也可以自定义bean的名字。如果当注解中只有一个属性时,其value属性的属性名可以省略不写。

// @Service("service")
@Service(value = "service")
public class UserServiceImpl implements UserService {
}

1.3.2 自动装配

在成员变量上使用@Autowired注解来完成自动装配,不在需要setXxx()方法了。
注意:自动装配前必须要扫描所用到的所有组件,将其装入IOC容器。

在controller层装配Service

@Controller("controller")
public class UserController {

    @Autowired
    private UserService userService;
    public void add(){
        userService.add();
    }
}

在service层装配Dao

@Service(value = "useService")
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao userDao;
    @Override
    public void add() {
        userDao.insert();
    }
}

注意:@Autowired注解还可以标记在构造器和setXxx()方法上。

@Autowired注解的工作流程

在这里插入图片描述
注意: @Qualifier 注解和 @Autowired注解要搭配使用,可以只有@Autowired注解,但是不能只有 @Qualifier 注解。

① 首先到IOC容器中查找所需要的组件类型
	如果能找到唯一的bean,则进行装配;
	如果找不到这个bean,则装配失败;
	如果有多个相同类型的bean的话
		若有 @Qualifier 注解:则需要根据 @Qualifier 注解中指定的名称作为bean的id或name进行匹配
			如果找到,则匹配成功;找不到,则匹配失败。
		若没有 @Qualifier 注解:则需要按照被加了@Autowired注解的属性的名字进行匹配
			如果找到,则匹配成功;找不到,则匹配失败。			

提醒@Autowired注解设置required = false属性表示:能装就装,装不上就不装

1.3.3 完全注解开发

创建组件类:

@Component
public class CommonComponents {
}

创建配置类:

使用@Configuration注解将一个普通的类标记为Spring的配置类

@Configuration
// 配置自动扫描包
@ComponentScan("com.zhang")
public class MyConfiguration {

	//  @Bean注解相当于XML配置文件中的bean标签
	//  @Bean注解标记的方法的返回值会被放入IOC容器
    @Bean(name = "abc")
    public CommonComponents getComponent() {
        CommonComponents commonComponent = new CommonComponents();
        System.out.println("数码宝贝");
        return commonComponent;
    }
}

测试

 public static void main(String[] args) {
        // AnnotationConfigApplicationContext根据配置类创建IOC容器对象
        ApplicationContext context= new AnnotationConfigApplicationContext(MyConfiguration.class);
        context.getBean("abc");
    }

1.3.4 整合junit4

向spring.xml文件中加入依赖

 		<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.5</version>
        </dependency>

注意:此依赖的版本号要与spring整体的依赖要保持一致,要不然会报错。针对于这个情况,我们以后可以在pom.xml文件中这样配置spring的相关依赖,可以更方便的管理spring相关的版本。

	<properties>
        <spring-version>5.3.5</spring-version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring-version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring-version}</version>
        </dependency>
    </dependencies>

junit的使用

// junit的@RunWith注解:指定Spring为Junit提供的运行器
@RunWith(SpringJUnit4ClassRunner.class)
// Spring的@ContextConfiguration指定Spring配置文件的位置
@ContextConfiguration(value = {"classpath:spring.xml"})
public class TestJunit {

    @Autowired
    private UserController userController;

    @Test
    public void test(){
        userController.add();
    }
}

使用junit的好处:
①:不需要之间创建IOC容器对象了。
②:任何的bean都可以在测试类中直接进行自动装配。

第三章 AOP

3.1 代理模式

3.1.1 代理模式的简介

代理模式是一种常用的Java设计模式。代理模式就是为一个对象提供一个代理类来扩展自己本身的功能。也就是说通过代理类来访问此对象中的目标方法。例如:广告商找明星拍广告需要通过经纪人。

下面是用图来描述一下。
在这里插入图片描述

按照代理的创建时期,代理类可以分为两种。

  1. 静态代理:静态代理的代理类在编译期间生成了.class文件
  2. 动态代理:动态代理的代理类在Java运行时通过反射动态生成。

3.1.2 静态代理

编写接口

public interface IPlay {
    void play(String gameName);
}

编写接口的实现类(被代理类)

public class Play implements IPlay{
    @Override
    public void play(String gameName) {
        System.out.println();
        System.out.println("我在玩" + gameName);
        System.out.println();
    }
}

编写静态代理类

public class PlayProxy implements IPlay{
	// 将被代理对象声明为成员变量
    private IPlay play;

    public PlayProxy(IPlay play) {
        this.play = play;
    }

    @Override
    public void play(String gameName) {
        System.out.println("调用被代理类方法之前做的准备工作");
        // 调用被代理对象执行的目标方法
        play.play(gameName);
        System.out.println("调用被代理类方法完成");
    }
}

测试:

public class Test {
    public static void main(String[] args) {
        IPlay play = new Play();
        PlayProxy playProxy = new PlayProxy(play);
        playProxy.play("王者");
    }
}

3.1.3 动态代理

概念:根据代理类对象的接口,动态创建代理类对象

有两种方法实现动态代理:

  • 通过jdk本身来实现动态代理
  • 通过cglib实现动态代理
3.1.3.1 通过jdk实现动态代理

通过图来理解jdk实现的动态代理
在这里插入图片描述
实现步骤:
①:编写代理类,实现接口 InvocationHandler 用来实现回调
②:声明属性,用来接收所有类型对象(被代理类对象)
③:编写方法根据被代理类对象动态创建代理类对象
④:重写 InvocationHandler 接口中的 invoke 方法,通过反射调用原有业务方法,做增强功能。

编写代理类

public class ProxyBean implements InvocationHandler {

    // 声明被代理类对象
    private Object biz;

    public Object newProxyInstance(Object biz){
        this.biz = biz;
        // 创建代理类对象需要三个参数:
        // ①:加载目标对象的类的类加载器
        // ②:目标对象所实现的所有接口组成的数组
        // ③:需要 InvocationHandler 对象,因为本类是 InvocationHandler接口的实现类,所以传入本类对象即可
        return Proxy.newProxyInstance(biz.getClass().getClassLoader(), biz.getClass().getInterfaces(),this);
    }

    /**
     *
     * @param proxy     代理对象,但是这里未用到 proxy(代理对象)
     * @param method    代表目标方法的Method对象
     * @param args      外部调用目标方法时传入的实际参数
     * @return
     * @throws Throwable
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("方法名为:" + method.getName());
        // invoke方法中的第一个参数是调用目标方法的目标对象,即声明的成员变量。
        //             第二个参数是外部调用目标方法时传入的参数。
        // 声明这个变量来接收目标方法的返回值
        Object targetValue = method.invoke(biz,args);
        return targetValue;
    }
}

测试

public class Test {
    public static void main(String[] args) {
   		// 创建被代理的对象
        IPlay plays = new Play();
        // 创建代理类
        ProxyBean proxyBean = new ProxyBean();
        // 通过代理类来创建被代理类的对象
        IPlay play = (IPlay)proxyBean.newProxyInstance(plays);
        // 通过代理类的对象调用被代理类对象的方法
        play.play("王者");
    }
}
3.1.3.2 通过cglib实现动态代理

通过图来理解cglib实现的动态代理
在这里插入图片描述

注意: 在编写业务类的时候不需要实现接口了。

编写业务类

public class Play {
    public void play(String gameName) {
        System.out.println();
        System.out.println("我在玩" + gameName);
        System.out.println();
    }
}

在pom.xml文化中加入依赖

		<dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.3.0</version>
        </dependency>

编写代理类

public class CglibProxyBean implements MethodInterceptor {

    /**
     * 根据被代理对象,动态生成代理类对象(被代理类的子类)
     * @param biz   传入被代理类对象
     * @return
     */
    public Object newProxyInstance(Object biz){
        // 创建动态类对象
        Enhancer enhancer = new Enhancer();
        // 这个是将代理类对象设置为被代理类对象的子类
        // 即代理类对象作为子类,被代理类对象作为父类
        enhancer.setSuperclass(biz.getClass());
        // 设置回调
        enhancer.setCallback(this);
        return enhancer.create();
    }

    /**
     *
     * @param o             代理对象
     * @param method        被代理对象的目标方法
     * @param objects       调用被代理方法时传入的参数
     * @param methodProxy   生成的代理类
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("========前置增强========");
        System.out.println(method.getName());
        Object targetValue = methodProxy.invokeSuper(o, objects);
        System.out.println("========后置增强========");
        return targetValue;
    }
}

测试

public class Test {
    public static void main(String[] args) {
        IPlay play = new Play();
        CglibProxyBean cglibProxyBean = new CglibProxyBean();
        play = (IPlay)cglibProxyBean.newProxyInstance(play);
        play.play("王者");
    }
}
3.1.3.3 jdk和cglib实现动态代理的区别

①:实现方式不同
jdk实现动态代理必须依赖接口
cglib实现动态代理是通过类来实现,即代理类要作为被代理类的子类(代理类继承被代理类)。

②:实现原理不同
jdk的动态代理是利用反射机制生成一个实现代理接口的代理类,在调用具体方法前调用InvokeHandler方法来处理。
cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。

③:性能不同
jdk1.6和1.7的时候,jdk实现动态代理要比cglib实现动态代理的速度慢
jdk1.8的时候,jdk实现动态代理要比cglib实现动态代理的速度快

关于使用上的注意点:
如果被代理类实现了接口,则Spring会默认使用jdk动态代理
如果被代理类未实现接口,则Spring要采用cglib动态代理
如果被代理类实现了接口,Spring也可以强制使用cglib动态代理
如果被代理类被final修饰,则不能采用cglib动态代理

3.2 AOP的核心套路

在这里插入图片描述

3.3 AOP术语

3.3.1 切面

概念:把所有通知方法封装到一个类中。
在这里插入图片描述

3.3.2 横切关注点

概念:从每个方法中抽取出来的同一类非核心业务。在未改变原有功能的基础上,额外加了几个功能,就有几个横切关注点。
下面用图来可以更好理解。
在这里插入图片描述

3.3.2 连接点

概念:将方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴方向,x轴和y轴的相交点就是连接点。即执行通知的方法

下面用图表示:
在这里插入图片描述

3.3.3 切入点

切入点也可以这样理解:在所有的方法中都可以使用增强通知,但你只需要其中的一部分的方法使用增强通知,其余的方法不需要使用增强通知,因此这些部分方法使用了增强通知的地方叫做切入点。

注意:AOP技术是通过切入点来定位连接点。

下面用图来理解:
在这里插入图片描述

3.3.4 增强通知

在每一个横切关注点上要做的事情需要写一个方法来实现,这样的方法就叫增强通知

  • 前置通知:在被代理的目标方法前执行。
  • 后置通知:在被代理的目标方法结束后执行。
  • 异常通知:在被代理的目标方法执行过程中抛出异常时执行。
  • 最终通知:在被代理的目标方法执行的过程中,不管是否抛出异常,都会执行。
  • 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知。

在这里插入图片描述

3.3.5 织入

将通知方法(切面)应用到被代理对象(目标对象)中。

3.4 AOP的使用

在pom.xml文件中导入依赖

		<dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.7</version>
        </dependency>

3.4.1 切入点表达式的语法

execution(public * com.zhang..*.*(..))

execution()是最常用的切点函数,其语法如下所示:

整个表达式可以分为五个部分:
①:execution():表达式主体
②:第一个 * 号表示返回类型* 表示所有的类型
③:包名表示需要拦截的包名后面的两个句点表示当前包和当前包的所有子包。
④:第二个 * 号表示类名, * 号表示所有的类
⑤:*(…)最后这个 * 号表示方法名* 号表示所有的方法后面括号里面表示方法的参数,两个句点表示任何参数

注意点:

  • 首先要特别注意 * 后是否有空格,有无空格的含义相差很大。
  • * 号代替 权限修饰符和返回值部分表示权限修饰符和返回值不限
  • 在包名的部分,一个 * 号只能代表包的层次结构中的一层,表示这一层是任意的。
  • 在包名的部分,使用 . . 表示包名任意、包的层次深度任意
  • 在类名的部分,类各部分整体用 * 代替,表示类名任意
  • 在类名的部分,可以使用 *代替类名的一部分
  • 在方法名部分,可以用 *表示方法名任意
  • 在方法名部分,可以用 *代替方法名的一部分
  • 在方法参数列表部分,使用 (…) 表示参数列表任意
  • 在方法参数列表部分,使用 (int , …) 表示参数列表以一个int类型的参数开头。
  • 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的。
  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符。
  • public * 表示权限修饰符明确,返回值任意是可以的。

例子:
①:表示匹配所有名称以Dao结尾的类或接口

public * com.zhang.*Dao..*.*(..)

②:表示匹配所有名称以Test结尾的方法

public * com.zhang.Dao..*Test.*(..)

③:表示匹配所有方法中第一个参数类型为int的方法

public * com.zhang.Dao..*.*(int,..)

④:表示匹配所有方法的返回值类型为int的方法

public int com.zhang.Dao..*.*(..)

3.4.2 通过代理实现AOP

五个通知(增强类):

通知执行时间实现方式
前置通知在运行目标方法之前执行的实现MethodBeforeAdvice接口,并重写before()方法
后置通知在运行目标方法之后执行的,如果运行目标方法过程中出现异常,则不会执行后置通知实现AfterReturningAdvice接口,并重写afterReturning()方法
异常通知在运行目标方法时出现异常执行的实现ThrowsAdvice接口,无重写方法
环绕通知在环绕通知中可以定义前置通知、后置通知、异常通知、最终通知实现MethodInterceptor接口,并重写invoke()方法;
最终通知运行完方法后,不管是否发生异常,都会执行

AdviceBefore(前置通知)

public class AdviceBefore implements MethodBeforeAdvice {
    private Logger logger = Logger.getLogger(AdviceBefore.class);
    /**
     * 功能: 日期+类+方法+参数
     * @param method		将来调用的方法
     * @param objects		方法的传入参数
     * @param o				目标对象
     * @throws Throwable
     */
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        logger.info("---------前置通知开始----------");
        logger.info(new Date() + ": " + o.getClass().getName() + "." + method.getName());
        logger.info("参数有: " + Arrays.toString(objects));
        logger.info("---------前置通知完成---------");
    }
}

AfterReturningAdvice(后置通知)

public class AdviceAfter implements AfterReturningAdvice{
    private Logger logger = Logger.getLogger(AdviceBefore.class);
    /**
     * 功能: 日期+类+方法+参数
     * @param o         方法的返回值
     * @param method    将来调用的方法
     * @param objects   方法的传入参数
     * @param o1        目标对象
     * @throws Throwable
     */
    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
            logger.info("---------后置通知开始----------");
            logger.info(new Date() + ": " + o1.getClass().getName() + "." + method.getName());
            logger.info("返回值:" + o);
            logger.info("参数有: " + Arrays.toString(objects));
            logger.info("---------后置通知完成---------");
    }
}

ThrowsAdvice(异常通知)

public class AdviceThrows implements ThrowsAdvice {
    private Logger logger = Logger.getLogger(AdviceThrows.class);
    /**
     * @param method    将来调用的方法
     * @param args      方法的传入参数
     * @param target    目标对象
     * @param e         
     */
    public void afterThrowing(Method method, Object[] args, Object target, Exception e) {
        logger.info("---------异常通知开始----------");
        logger.info(new Date() + ": " + target.getClass().getName() + "." + method.getName());
        logger.info("参数有: " + Arrays.toString(args));
        logger.info("异常信息为:" + e.getMessage());
        logger.info("---------异常通知完成---------");
    }
}

MethodInterceptor(环绕通知)

public class AdviceAround implements MethodInterceptor {
    private Logger logger = Logger.getLogger(AdviceBefore.class);
    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        logger.info("---------环绕通知开始----------");
        try {
            logger.info("全类名为:" + methodInvocation.getThis().getClass().getName());
            // 获取方法
            Method method = methodInvocation.getMethod();
            logger.info("方法名:" + method.getName());
            // 获取参数
            Object[] arguments = methodInvocation.getArguments();
            logger.info("参数有:" + Arrays.toString(arguments));
            // 必须要使用methodInvocation.proceed(),可以决定业务类方法执行的位置
            logger.info("返回值为:" + methodInvocation.proceed());
        } catch (Throwable throwable) {
            logger.info("异常信息为:" + throwable.getMessage());
        } finally {
            logger.info("环绕通知中的最终通知");
        }
        logger.info("---------环绕通知完成---------");
        return null;
    }
}

编写业务类

// 业务的接口
public interface IPlay {
    void play(String gameName);
}
// 实现业务的接口
public class Play implements IPlay{
    @Override
    public void play(String gameName) {
        System.out.println();
       // int i = 10 / 0;
        System.out.println("我在玩" + gameName);
        System.out.println();
    }
}

配置spring.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 业务bean -->
    <bean id="play" class="com.zhang.test.Play"/>
    <!-- 前置通知 -->
    <bean id="adviceBefore" class="com.zhang.aop.AdviceBefore"/>
    <!-- 后置通知 -->
    <bean id="adviceAfter" class="com.zhang.aop.AdviceAfter"/>
    <!-- 异常通知 -->
    <bean id="adviceThrow" class="com.zhang.aop.AdviceThrows"/>
    <!-- 环绕通知 -->
    <bean id="adviceAround" class="com.zhang.aop.AdviceAround"/>
    <!-- 代理bean -->
    <bean id="proxyFactoryBean" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!-- target属性:指定要代理的目标类-->
        <property name="target" ref="play"/>
        <!-- proxyInterfaces属性:指定要代理的接口(即代理要实现的接口)-->
        <property name="proxyInterfaces" value="com.zhang.test.IPlay"/>
        <!-- proxyTargetClass属性:如果value为true,则使用cglib;如果value为false,则使用jdk动态代理-->
        <property name="proxyTargetClass" value="true"/>
        <!-- interceptorNames属性:指定要在代理的目标类中添加的功能-->
        <property name="interceptorNames">
            <list>
                <list>
                <!-- 一般情况配置了环绕通知就不需要配置前置通知、异常通知、后置通知了 -->
                <value>adviceAround</value>
                <value>adviceThrow</value>
                <!--<value>adviceBefore</value>
                <value>adviceAfter</value>-->
            </list>
            </list>
        </property>
    </bean>
</beans>

3.4.3 通过SpringAPI接口实现AOP

与通过代理实现AOP对比,它俩不同的是spring.xml文件,增强类和业务类是相同的。
注意:一般情况配置了环绕通知就不需要配置前置通知、异常通知、后置通知了

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 业务bean -->
    <bean id="play" class="com.zhang.test.Play"/>
    <!-- 前置通知 -->
    <bean id="adviceBefore" class="com.zhang.aop.AdviceBefore"/>
    <!-- 后置通知 -->
    <bean id="adviceAfter" class="com.zhang.aop.AdviceAfter"/>
    <!-- 异常通知 -->
    <bean id="adviceThrow" class="com.zhang.aop.AdviceThrows"/>
    <!-- 环绕通知 -->
    <bean id="adviceAround" class="com.zhang.aop.AdviceAround"/>
 
    <!-- 配置aop,并且导入aop的命名空间-->
    <aop:config>
        <!-- 切入点(连接点的集合)  连接点 执行通知的方法 -->
        <aop:pointcut expression="execution(public * com.zhang.test..*.*(..))" id="pointcut"/>
        <!--
            定义AOP通知器,这是一种特殊的aspect;
            其advice-ref属性是用来配置通知
            pointcut-ref属性是用来配置切点
        -->
       <!-- <aop:advisor advice-ref="adviceBefore" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="adviceAfter" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="adviceThrow" pointcut-ref="pointcut"/>-->
        <aop:advisor advice-ref="adviceAround" pointcut-ref="pointcut"/>
    </aop:config>
</beans>

3.4.4 通过自定义类实现AOP

自定义的LogAdvice类:
注意:在通知方法的形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入。通过JoinPoint对象就可以获取目标方法名、实参数列表等。

public class LogAdvice {
    private Logger logger = Logger.getLogger(LogAdvice.class);

    /**
     * 前置通知
     * @param joinPoint
     */
    public void before(JoinPoint joinPoint){
        logger.info("****************前置增强处理开始**************");
        logger.info("全类名为:" + joinPoint.getTarget().getClass().getName());
        // getSignature()可以获取目标方法签名对象,即一个方法的全部声明信息
        // 通过方法的签名对象获取目标方法的详细信息
        logger.info("方法名为:" + joinPoint.getSignature().getName());
        // 通过JoinPoint对象获取外界调用目标方法时传入的实参列表
        logger.info("参数为:" + Arrays.toString(joinPoint.getArgs()));
        logger.info("****************前置增强处理结束**************");
    }

    /**
     * 后置通知
     * @param joinPoint
     */
    public void after(JoinPoint joinPoint){
        logger.info("****************最终增强处理开始**************");
        logger.info("全类名为:" + joinPoint.getTarget().getClass().getName());
        logger.info("方法名为:" + joinPoint.getSignature().getName());
        logger.info("参数为:" + Arrays.toString(joinPoint.getArgs()));
        logger.info("****************最终增强处理结束**************");
    }

    /**
     * 最终通知
     * @param joinPoint
     * @param val
     */
    public void afterReturning(JoinPoint joinPoint,Object val){
        logger.info("****************后置增强处理开始**************");
        logger.info(val);
        logger.info("****************后置增强处理结束**************");
    }

    /**
     * 异常通知
     * @param joinPoint
     * @param exception
     */
    public void except(JoinPoint joinPoint,Exception exception){
        logger.info("****************异常增强处理开始**************");
        logger.info(exception.getMessage());
        logger.info("****************异常增强处理结束**************");
    }

    /**
     * 环绕通知
     * @param joinPoint
     */
    public void around(ProceedingJoinPoint joinPoint){
        logger.info("****************环绕增强处理开始**************");
        logger.info("全类名为:" + joinPoint.getTarget().getClass().getName());
        logger.info("方法名为:" + joinPoint.getSignature().getName());
        logger.info("参数为:" + Arrays.toString(joinPoint.getArgs()));
        try {
            logger.info("****************前置增强处理**************");
            // 原有业务执行
            Object returnVal = joinPoint.proceed();
            logger.info("****************后置增强处理**************" + "  返回值:" + returnVal);
        } catch (Exception e) {
            e.getMessage();
            logger.info("****************异常增强处理**************");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            logger.info(throwable.getMessage());
        } finally {
            logger.info("****************最终增强处理**************");
        }
        logger.info("****************环绕增强处理结束**************");
    }
}

编写spring.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 业务bean -->
    <bean id="play" class="com.zhang.test.Play"/>
    <!-- 配置切面类的bean -->
    <bean id="logAdvice" class="com.zhang.aop.LogAdvice"/>
    <!-- 配置AOP -->
    <aop:config>
        <!-- 配置切入点表达式 -->
        <aop:pointcut id="pointcut" expression="execution(public * com.zhang.test..*.*(..))"/>
        <!-- aop:aspect标签:配置切面 -->
        <!-- ref属性:关联切面类的bean -->
        <aop:aspect ref="logAdvice">
            <!-- aop:before标签: 配置前置通知 -->
            <!-- method属性:指定前置通知的方法名 -->
            <!-- pointcut-ref属性:引用切入点表达式 -->
            <aop:before method="before" pointcut-ref="pointcut"/>
            
            <!-- aop:after标签:配置最终通知 -->
            <aop:after method="after" pointcut-ref="pointcut"/>
            
            <!-- aop:after-returning标签:配置后置通知 -->
            <!-- returning属性:指定通知方法中用来接收目标方法返回值的参数名 -->
            <aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="val"/>
            
            <!-- aop:after-throwing标签:配置异常通知 -->
            <!-- throwing属性:指定的通知方法中用来接收目标方法抛出异常的异常对象的参数名-->
            <aop:after-throwing method="except" throwing="exception" pointcut-ref="pointcut"/>
            
            <!-- aop:around标签:配置环绕通知-->
            <aop:around method="around" pointcut-ref="pointcut"/>
        </aop:aspect>
    </aop:config>
</beans>

3.4.5 通过注解实现AOP

在spring.xml文件中配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

       <!-- 配置自动扫描的包 -->
       <context:component-scan base-package="com.zhang.test"/>
       <!-- 启用基于注解的AOP功能 -->
       <aop:aspectj-autoproxy/>
</beans>

编写业务类
跟之前不同的是: 使用注解实现时不需要实现接口。

public class Play{
    public void play(String gameName) {
        System.out.println();
       // int i = 10 / 0;
        System.out.println("我在玩" + gameName);
        System.out.println();
    }
}

自定义通知类添加注解

// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
public class LogAdvice {
    private Logger logger = Logger.getLogger(LogAdvice.class);
	// 切入点表达式重用
    @Pointcut("execution(public * com.zhang.test..*.*(..))")
    public void myPoint(){}

	// @Before注解:声明当前方法是前置通知方法
    // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
    // 第一种方法是写切入点表达式
    // @Before(value = "execution(public * com.zhang.test..*.*(..))")
    // 第二种方法是使用已经定义好的切入点表达式,
    // 注意:使用切入点表达式不仅可以使用本类中定义好的,也可以使用其他类中的。
    @Before(value = "myPoint()")
    public void before(JoinPoint joinPoint){
        logger.info("****************前置增强处理开始**************");
        logger.info("全类名为:" + joinPoint.getTarget().getClass().getName());
        logger.info("方法名为:" + joinPoint.getSignature().getName());
        logger.info("参数为:" + Arrays.toString(joinPoint.getArgs()));
        logger.info("****************前置增强处理结束**************");
    }

	
	// @After注解标记最终通知方法
    //@After(value = "execution(public * com.zhang.test..*.*(..))")
    @Before(value = "myPoint()")
    public void after(JoinPoint joinPoint){
        logger.info("****************最终增强处理开始**************");
        logger.info("全类名为:" + joinPoint.getTarget().getClass().getName());
        logger.info("方法名为:" + joinPoint.getSignature().getName());
        logger.info("参数为:" + Arrays.toString(joinPoint.getArgs()));
        logger.info("****************最终增强处理结束**************");
    }

	// @AfterReturning注解标记后置通知方法
	// 注意:要在@AfterReturning注解中通过returning属性设置形参名称,
	// 并且要在通知方法中声明一个对应的形参,
	// 其方法中形参的名称要与returning属性设置的名称要相对应,Spring会将目标方法的返回值从这里传给我们
    @AfterReturning(value = "execution(public * com.zhang.test..*.*(..))",returning = "val")
    public void afterReturning(JoinPoint joinPoint,Object val){
        logger.info("****************后置增强处理开始**************");
        logger.info(val);
        logger.info("****************后置增强处理结束**************");
    }

	
	// @AfterThrowing注解标记异常通知方法
	// 在@AfterThrowing注解中声明一个throwing属性设置形参名称
	// 并且在通知方法中声明一个对应的形参,
	// 其方法中的形参名称就是在throwing属性中所设置的,
	// Spring会将目标方法抛出的异常对象从这里传给我们
    @AfterThrowing(value = "execution(public * com.zhang.test..*.*(..))",throwing = "exception")
    public void except(JoinPoint joinPoint,Exception exception){
        logger.info("****************异常增强处理开始**************");
        logger.info(exception.getMessage());
        logger.info("****************异常增强处理结束**************");
    }

   // 使用@Around注解标明环绕通知方法
   // 在通知方法形参位置声明ProceedingJoinPoint类型的形参,Spring会将这个类型对象传给我们
   // @Around(value = "execution(public * com.zhang.test..*.*(..))")
    public void around(ProceedingJoinPoint joinPoint){
        logger.info("****************环绕增强处理开始**************");
        logger.info("全类名为:" + joinPoint.getTarget().getClass().getName());
        // 通过getSignature()方法获取签名对象,然后通过签名对象获取目标方法的方法名
        logger.info("方法名为:" + joinPoint.getSignature().getName());
        // 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
        logger.info("参数为:" + Arrays.toString(joinPoint.getArgs()));
        try {
            logger.info("****************前置增强处理**************");
            // 通过ProceedingJoinPoint对象让原有业务执行
            // 声明变量用来存储目标方法的返回值
            Object returnVal = joinPoint.proceed();
            logger.info("****************后置增强处理**************" + "  返回值:" + returnVal);
        } catch (Exception e) {
            e.getMessage();
            logger.info("****************异常增强处理**************");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            logger.info(throwable.getMessage());
        } finally {
            logger.info("****************最终增强处理**************");
        }
        logger.info("****************环绕增强处理结束**************");
    }
}

第四章 声明式事务

4.1 JDBCTemplate

Spring提供的数据访问模板,提供了很多方便操作数据库的方法,例如JDBCTemplate。

4.1.1 实现步骤

在pom.xml文件中添加依赖

	<dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
            <version>5.3.10</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.10</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.26</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
        </dependency>
    </dependencies>

编写实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
    private Integer bookid;
    private String bookname;
    private Float price;
    private String author;
    private Date pubDate;
}

编写Dao接口

public interface BookDao {
    List<Book> queryAll();
}

编写使用Dao接口的类

@Repository
public class BookDao implements IBookDao {

    @Autowired
    private JdbcTemplate template;

    @Override
    public List<Book> queryAll() {
        String sql = "select bookid,bookname,pubDate,price,author from t_book";
        return template.query(sql,new BookRowMapper());
    }

    class BookRowMapper implements RowMapper<Book>{

        @Override
        public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
            Book book = new Book();
            book.setBookid(rs.getInt("bookid"));
            book.setBookname(rs.getString("bookname"));
            book.setPrice(rs.getFloat("price"));
            book.setAuthor(rs.getString("author"));
            book.setPubDate(rs.getDate("pubDate"));
            return book;
        }
    }
}

编写spring-conf.xml的配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <context:component-scan base-package="com.zhang"/>
    <bean name="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="username" value="root"></property>
        <property name="password" value="123456"></property>
        <property name="url" value="jdbc:mysql://127.0.0.1:3306/testdb">
        </property>
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver">
        </property>
    </bean>
    <bean name="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>

编写测试类

	@Test
    public void test1(){
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
        IBookDao bookDao = context.getBean("bookDao", IBookDao.class);
        bookDao.queryAll().forEach(System.out::println);
    }

4.1.2 基本使用

4.1.2.1 增删改查

IBookDao接口中

public interface IBookDao {
    String get(Integer bookid);
    void save(Book book);
    Integer update(Book book);
    Integer delete(Integer bookid);
}

IBookDao接口的实现类中

@Repository
public class BookDao implements IBookDao {

    @Autowired
    private JdbcTemplate template;
	// 根据bookid查询图书名字
    @Override
    public String get(Integer bookid) {
        String sql = "select bookname from t_book where bookid = ?";
        return  template.queryForObject(sql,String.class,1);
    }
	// 添加图书
    @Override
    public void save(Book book) {
        String sql = "insert into t_book (bookname,pubDate,price,author) values (?,?,?,?)";
        template.update(sql,book.getBookname(),book.getPubDate(),book.getPrice(),book.getAuthor());
    }
	// 更新图书
    @Override
    public Integer update(Book book) {
        String sql = "update t_book set bookname = ? where bookid = ?";
        return template.update(sql,book.getBookname(),book.getBookid());
    }
	// 根据id删除图书
    @Override
    public Integer delete(Integer bookid) {
        String sql = "delete from t_book where bookid = ?";
        return template.update(sql,bookid);
    }
}

测试类

	@Test
    public void test2(){
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
        IBookDao bookDao = context.getBean("bookDao", IBookDao.class);
        System.out.println(bookDao.get(1));
    }

    @Test
    public void test3() {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
        IBookDao bookDao = context.getBean("bookDao", IBookDao.class);
        bookDao.save(new Book(null,"C++",65f,"王五",new Date()));
    }

    @Test
    public void test4() {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
        IBookDao bookDao = context.getBean("bookDao", IBookDao.class);
        Book book = new Book();
        book.setBookid(1);
        book.setBookname("Spring");
        bookDao.update(book);
    }

    @Test
    public void test5() {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
        IBookDao bookDao = context.getBean("bookDao", IBookDao.class);
        bookDao.delete(2);
    }
4.1.2.2 使用BeanPropertyRowMapper类进行查询

在IBookDao的接口中

public interface IBookDao {
    List<Book> queryAll();
}

在IBookDao接口的实现类中

@Repository
public class BookDao implements IBookDao {

    @Autowired
    private JdbcTemplate template;

    @Override
    public List<Book> queryAll() {
        String sql = "select bookid,bookname,pubDate,price,author from t_book";
        return template.query(sql,new BeanPropertyRowMapper<>(Book.class));
    }
}
4.1.2.3 进行批量插入

在IBookDao的接口中

public interface IBookDao {
    void batchInsert(List<Book> list);
}

在IBookDao接口的实现类中

@Repository
public class BookDao implements IBookDao {

    @Autowired
    private JdbcTemplate template;

     @Override
    public void batchInsert(List<Book> list) {
        String sql = "insert into t_book (bookname,price,author,pubDate)values(?,?,?,?)";

        List<Object[]> params = new ArrayList<>();

        for (Book book : list) {
            Object[] param = {book.getBookname(),book.getPrice(),book.getAuthor(),book.getPubDate()};
            params.add(param);
        }
        template.batchUpdate(sql, params);
    }
}

测试类:

	@Test
    public void test6() {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
        IBookDao bookDao = context.getBean("bookDao", IBookDao.class);
        List<Book> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Book book = new Book();
            book.setBookname("Java" + i);
            book.setPubDate(new Date());
            book.setAuthor("王五" + i);
            book.setPrice(65 + 1f);
            list.add(book);
        }
        bookDao.batchUpdate(list);
    }

4.2 声明式事务概念

4.2.1 编程式事务

事务功能的相关操作全部通过自己编写代码来实现:

Connection conn = ...;   
try {   
    // 开启事务:关闭事务的自动提交
    conn.setAutoCommit(false);    
    // 核心操作   
    // 提交事务
    conn.commit();   
}catch(Exception e){   
    // 回滚事务
    conn.rollBack();    
}finally{    
    // 释放数据库连接
    conn.close();   
}

缺陷:需要开发人员手动来完成一些细节上的操作,如果开发人员不能将一些代码有效的抽取出来,代码就不能得到高效利用,每次实现功能时都需要自己手动编写额外的代码。

4.2.2 声明式事务

由于编程式事务的缺陷,我们可以使用框架将固定模式的代码抽取出来,进行相关的封装。此时,我们只需要在配置文件中进行简单的配置即可完成操作。

好处:
①:提高了开发效率
②:消除了冗余的代码

声明式事务和编程式事务的区别:

  • 编程式自己写代码完成功能
  • 声明式通过配置让框架实现功能

4.2.3 事务管理器

在这里插入图片描述
我们在Spring使用的事务管理器是org.springframework.jdbc.datasource.DataSourceTransactionManager,之后整合MyBatis用的也是这个类。

DataSourceTransactionManager类中的方法:
在这里插入图片描述
我们可以看到事务的一些主要方法:

  • doBegin():开启事务
  • doSuspend():挂起事务
  • doResume():恢复挂起的事务
  • doCommit():提交事务
  • doRollback():回滚事务

4.3 基于注解的声明式事务

4.3.1 准备工作

这里我们还是用 JDBCTemplate部分 准备工作的数据,再次之外,我们还会加入业务逻辑层,在业务逻辑层中使用事务。

下来会测试Spring在默认情况下的事务是否会生效。

准备 IBookDao接口中添加方法

Integer updateMoney(Integer bookid,String money);

实现IBookDao接口,因为这里是在做测试,所以将下面的Sql语句写错,为了便于测试默认情况下事务是否会生效。

	@Override
    public Integer updateMoney(Integer bookid,String money) {
    	// 这里故意将sql语句写错。
        String sql = "update t1_book set price = ? where bookid = ?";
        return template.update(sql,money,bookid);
    }

编写IBookService接口

void updateTwice(Integer bookid,String bookName,String money);

实现IBookService接口

 	@Autowired
    private IBookDao bookDao;

    @Override
    public void updateTwice(Integer bookid, String bookName, String money) {
        Book book = new Book();
        book.setBookid(bookid);
        book.setBookname(bookName);
         // 第一次修改书名
        bookDao.update(book);
        // 第二次修改金额
        bookDao.updateMoney(bookid,money);
    }

添加日志依赖

		<dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>

配置logback.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    <!-- 指定日志输出的位置 -->
    <appender name="STDOUT"
              class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- 日志输出的格式 -->
            <!-- 按照顺序分别是:时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 -->
            <pattern>[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n</pattern>
        </encoder>
    </appender>

    <!-- 设置全局日志级别。日志级别按顺序分别是:DEBUG、INFO、WARN、ERROR -->
    <!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 -->
    <root level="INFO">
        <!-- 指定打印日志的appender,这里通过“STDOUT”引用了前面配置的appender -->
        <appender-ref ref="STDOUT" />
    </root>
    <!-- 根据特殊需求指定局部日志级别 -->
    <logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager" level="DEBUG"/>
    <logger name="org.springframework.jdbc.core.JdbcTemplate" level="DEBUG" />

</configuration>

最终结果: 名字修改成功了,但是书的价格修改失败。因此在默认情况下,事务是不会开启的

4.3.2 添加事务

4.3.2.1 在spring-conf.xml文件中配置事务管理器
	<!-- 配置事务管理器 -->
    <bean name="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 事务管理器的bean只需要装配数据源,其他属性保持默认值即可 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>
4.3.2.2 开启基于注解的声明式事务功能
	<!-- 开启基于注解的声明式事务功能 -->
    <!-- 使用transaction-manager属性指定当前使用是事务管理器的bean -->
    <!-- transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>

这里给大家提醒一下,导入<tx:annotation-driven />这个的命名空间时要注意一下,千万不能导错,否则就会无用。

xmlns:tx="http://www.springframework.org/schema/tx"
4.3.2.3 在需要事务的方法上使用注解
	@Autowired
    private IBookDao bookDao;
	@Transactional
    public void updateTwice(Integer bookid, String bookName, Float money) {
        Book book = new Book();
        book.setBookid(bookid);
        book.setBookname(bookName);
        // 第一次修改书名
        bookDao.update(book);
        // 第二次修改金额
        bookDao.updateMoney(bookid,money);
    }
4.3.2.4 补充:也可以在需要事务的类上使用@Transactional注解

注意: @Transactional注解在类级别标记,会影响到类中的每一个方法。同时,类级别标记的@Transactional注解中设置的事务属性也会延续影响方法执行时的事务属性。除非在方法上又设置了@Transactional注解。

例:在BookService类上设置了@Transactional注解 readOnly 属性为 true,那么在这个类中有查询的方法都不需要设置@Transactional注解了,而增删改方法需要设置@Transactional注解 readOnly 属性为 false。

@Transactional(readOnly = true)
@Service
public class BookService implements IBookService {

    @Autowired
    private IBookDao bookDao;
    @Transactional(readOnly = false)
    @Override
    public void updateTwice(Integer bookid, String bookName, Float money) {
        Book book = new Book();
        book.setBookid(bookid);
        book.setBookname(bookName);
        // 第一次修改书名
        bookDao.update(book);
        // 第二次修改金额
        bookDao.updateMoney(bookid,money);
    }  
    @Override
    public String getName(Integer bookid) {
        return bookDao.get(bookid);
    }
}

最终结果: 在配置了事务之后,如果修改数据的途中发生了错误,则会出现事务的回滚,即数据修改失败。

4.3.3 设置事务的只读属性

概述:对于一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。

设置方式:在业务层所需要的方法上设置此属性。

提醒:从这里开始我们都是在方法上设置@Transactional注解,不在类上声明。

	@Autowired
    private IBookDao bookDao;
	 // readOnly = true 把当前事务设置为只读。
    @Transactional(readOnly = true)
    @Override
    public String getName(Integer bookid) {
        return bookDao.get(bookid);
    }

注意:只读属性只适用于查询操作,如果是增删改操作,会报异常。

Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

4.3.4 设置事务的超时属性

概述:事务在执行的过程中,会因为某些问题导致程序卡住,从而长时间占用数据库资源。那么此时出现问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。

也就是说:超时回滚,释放资源

设置方式:

在业务逻辑层中的方法上设置@Transactional注解的 timeout属性,让其中的IBookDao实现类的update方法睡眠,这里设置的休眠时间必须要比 timeout属性设置的时间要长。

	@Autowired
    private IBookDao bookDao;

    @Transactional(timeout = 3)
    @Override
    public void updateTwice(Integer bookid, String bookName, Float money) {
        Book book = new Book();
        book.setBookid(bookid);
        book.setBookname(bookName);
        // 第一次修改书名
        bookDao.update(book);
        // 第二次修改金额
        bookDao.updateMoney(bookid,money);
    }

下面sleep方法必须要放在执行SQL语句之前,要不然就不会起作用

	@Override
    public Integer update(Book book) {
       try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String sql = "update t_book set bookname = ? where bookid = ?";
        return template.update(sql,book.getBookname(),book.getBookid());
    }

测试时出现的异常信息:

org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Thu Aug 11 16:45:38 CST 2022

4.3.5 设置事务的回滚和不回滚的异常属性

  • rollbackFor属性:需要设置一个Class类型的对象
  • rollbackForClassName属性:需要设置一个字符串类型的全类名
4.3.5.1 默认情况

默认情况下对于运行时异常是回滚的,编译时异常不回滚

	@Override
    public Integer update(Book book) throws FileNotFoundException{
        String sql = "update t_book set bookname = ? where bookid = ?";
        int update = template.update(sql, book.getBookname(), book.getBookid());
        // 抛出编译时异常测试是否回滚 === 不回滚 
        new FileInputStream("abcd");
        // 抛出运行时异常测试是否回滚 == 回滚
        // System.out.println(10/0);
        return update;
    }
4.3.5.2 设置回滚的异常

设置形式:在@Transactional设置rollbackFor 属性

在出现编译时异常的情况下设置回滚属性
它这个原本是不回滚的,设置之后是回滚的。

	@Autowired
    private IBookDao bookDao;
    
	@Transactional(rollbackFor = Exception.class)
    @Override
    public Integer update(Book book) throws FileNotFoundException{
        int update = bookDao.update(book);
        new FileInputStream("abcd");
        return update;
    }
4.3.5.3 设置不回滚的异常

设置形式:在@Transactional设置noRollbackFor 属性

在出现运行时异常的情况下设置不回滚属性
它这个原本是回滚的,设置之后是不回滚的

	@Autowired
    private IBookDao bookDao;
    
	@Transactional(noRollbackFor = Exception.class)
    @Override
    public Integer update(Book book) throws FileNotFoundException{
        int update = bookDao.update(book);
        System.out.println(10/0);
        return update;
    }
4.3.5.4 同时设置回滚和不回滚异常

①:范围不同
不管是哪个设置范围大,都是在大范围内在排除小范围的设定
例如:rollbackFor = Exception.class ; noRollbackFor = FileNotFoundException.class ;
意思是除了 FileNotFoundException 之外,其他所有 Exception 范围的异常都回滚;但是碰到 FileNotFoundException 不回滚。此时小范围的优先级大于大范围的优先级。

②:范围一致
回滚和不回滚的异常设置了相同范围
例如:rollbackFor = Exception.class ; noRollbackFor = Exception.class ;

那么此时Spring会采纳 rollbackFor 设置的属性,遇到此范围内的异常会回滚。

4.3.6 设置事务的隔离级别属性

准备工作:
在这里插入图片描述

3.6.1 测试读未提交

业务层的方法:

	@Autowired
    private IBookDao bookDao;
    
	@Transactional(readOnly = true,isolation = Isolation.READ_UNCOMMITTED)
    @Override
    public String getName(Integer bookid) {
        String name = bookDao.get(bookid);
        return name;	// 这里打一个断点
    }
    @Transactional(readOnly = false,isolation = Isolation.READ_UNCOMMITTED)
    @Override
    public Integer updateName(Book book) throws FileNotFoundException{
        int update = bookDao.updateName(book);
        System.out.println(10/0);	
        return update;	// 这里打一个断点
    }

测试方法: 这里需要启动两个debug界面。运行之后就可以看到 getName()方法取的书名。

 	@Test
    public void test10() {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
        IBookService bookService = context.getBean("bookService", IBookService.class);
        Book book = new Book();
        book.setBookid(1);
        book.setBookname("Java9997878");
        bookService.updateName(book);
    }

    @Test
    public void test11()  {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
        IBookService bookService = context.getBean("bookService", IBookService.class);
        bookService.getName(1);
    }

测试结果执行查询操作的事务,它读取了另一个尚未提交的数据

3.6.2 测试读已提交

业务层的方法:

	@Autowired
    private IBookDao bookDao;
    
	@Transactional(readOnly = true,isolation = Isolation.READ_COMMITTED)
    @Override
    public String getName(Integer bookid) {
        String name = bookDao.get(bookid);
        return name;	// 这里打一个断点
    }
    @Transactional(readOnly = false,isolation = Isolation.READ_COMMITTED,,noRollbackFor = Exception.class)
    @Override
    public Integer updateName(Book book) throws FileNotFoundException{
        int update = bookDao.updateName(book);
        System.out.println(10/0);	
        return update;	// 这里打一个断点
    }

测试方法: 这里需要启动两个debug界面。运行之后就可以看到 getName()方法取的书名。

 	@Test
    public void test10() {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
        IBookService bookService = context.getBean("bookService", IBookService.class);
        Book book = new Book();
        book.setBookid(1);
        book.setBookname("Java9997878");
        bookService.updateName(book);
    }

    @Test
    public void test11()  {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
        IBookService bookService = context.getBean("bookService", IBookService.class);
        bookService.getName(1);
    }

测试结果执行查询操作的事务读取的是数据库中正确的数据

4.3.7 设置事务的传播行为属性

用图可能会更好理解。
在这里插入图片描述

4.3.7.1 propagation属性
名称含义
REQUIRED 默认值当前方法必须工作在事务中; 如果当前线程上有已经开启的事务可用,那么就在这个事务中运行;如果当前线程上没有已经开启的事务,那么就自己开启新事务,在新事务中运行;所以当前方法有可能和其他方法共用事务;在共用事务的情况下:当前方法会因为其他方法回滚而受连累
REQUIRES_NEW 建议使用当前方法必须工作在事务中 ;不管当前线程上是否有已经开启的事务,都要开启新事务; 在新事务中运行;不会和其他方法共用事务,避免被其他方法连累
4.3.7.2 测试

在这里插入图片描述

4.3.7.2.1 测试REQUIRED 模式

IBookService接口

public Integer updateName(Book book);

public Integer updateMoney(Book book);

实现IBookService接口的类

	@Transactional(readOnly = false,propagation = Propagation.REQUIRED)
    @Override
    public Integer updateName(Book book) {
        int count = bookDao.updateName(book);
        System.out.println(10/0);
        return count;
    }

    @Transactional(readOnly = false,propagation = Propagation.REQUIRED)
    @Override
    public Integer updateMoney(Book book) {
        Integer count = bookDao.updateMoney(book.getBookid(), book.getPrice());
        return count;
    }

编写TestService的类

@Service
public class TestService {
    @Autowired
    private IBookService bookService;
    @Transactional
    public void topTxMethod(){
        Book book = new Book();
        book.setBookid(4);
        book.setPrice(99f);
        bookService.updateMoney(book);
        book.setBookname("张五");
        bookService.updateName(book);
    }
}

编写测试类

	@Test
    public void test(){
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
        TestService bookService = context.getBean("testService", TestService.class);
        bookService.topTxMethod();
    }

事务的执行结果

[18:44:41.809] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Creating new transaction with name [com.zhang.service.impl.TopService.topTxMethod]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT]
[18:44:44.128] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd] for JDBC transaction]
[18:44:44.132] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd] to manual commit]
[18:44:44.155] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Suspending current transaction, creating new transaction with name [com.zhang.service.impl.BookService.updateMoney]]
[18:44:44.177] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234] for JDBC transaction]
[18:44:44.177] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234] to manual commit]
[18:44:44.180] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL update]
[18:44:44.182] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL statement [update t_book set price = ? where bookid = ?]]
[18:44:44.235] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Initiating transaction commit]
[18:44:44.235] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Committing JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234]]
[18:44:44.243] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234] after transaction]
[18:44:44.245] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Resuming suspended transaction after completion of inner transaction]
[18:44:44.247] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Suspending current transaction, creating new transaction with name [com.zhang.service.impl.BookService.updateName]]
[18:44:44.267] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6] for JDBC transaction]
[18:44:44.267] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6] to manual commit]
[18:44:44.271] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL update]
[18:44:44.271] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL statement [update t_book set bookname = ? where bookid = ?]]
[18:44:44.277] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Initiating transaction rollback]
[18:44:44.278] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6]]
[18:44:44.282] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6] after transaction]
[18:44:44.283] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Resuming suspended transaction after completion of inner transaction]
[18:44:44.283] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Initiating transaction rollback]
[18:44:44.284] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd]]
[18:44:44.285] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd] after transaction]

总结:
调用BookService层的两个方法都没有修改值,因此总事务回滚了。
注意:不能用try-catch处理异常,如果使用了try-catch,不管有没有捕获到异常,总事务总是提交。

4.3.7.2.2 测试REQUIRES_NEW模式

实现IBookService接口的类

	@Transactional(readOnly = false,propagation = Propagation.REQUIRES_NEW)
    @Override
    public Integer updateName(Book book) {
        int count = bookDao.updateName(book);
        System.out.println(10/0);
        return count;
    }

    @Transactional(readOnly = false,propagation = Propagation.REQUIRES_NEW)
    @Override
    public Integer updateMoney(Book book) {
        Integer count = bookDao.updateMoney(book.getBookid(), book.getPrice());
        return count;
    }

编写TestService的类

@Service
public class TestService {
    @Autowired
    private IBookService bookService;
    @Transactional
    public void topTxMethod(){
        Book book = new Book();
        book.setBookid(4);
        book.setPrice(99f);
        bookService.updateMoney(book);
        book.setBookname("张五");
        bookService.updateName(book);
    }
}

编写测试类

	@Test
    public void test(){
        ApplicationContext context = new ClassPathXmlApplicationContext("spring-conf.xml");
        TestService bookService = context.getBean("testService", TestService.class);
        bookService.topTxMethod();
    }

事务的执行结果

[20:02:15.051] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Creating new transaction with name [com.zhang.service.impl.TopService.topTxMethod]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT]
[20:02:17.687] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd] for JDBC transaction]
[20:02:17.692] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd] to manual commit]
[20:02:17.717] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Suspending current transaction, creating new transaction with name [com.zhang.service.impl.BookService.updateMoney]]
[20:02:17.740] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234] for JDBC transaction]
[20:02:17.740] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234] to manual commit]
[20:02:17.742] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL update]
[20:02:17.743] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL statement [update t_book set price = ? where bookid = ?]]
[20:02:17.811] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Initiating transaction commit]
[20:02:17.811] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Committing JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234]]
[20:02:17.813] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@14dda234] after transaction]
[20:02:17.815] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Resuming suspended transaction after completion of inner transaction]
[20:02:17.816] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Suspending current transaction, creating new transaction with name [com.zhang.service.impl.BookService.updateName]]
[20:02:17.839] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6] for JDBC transaction]
[20:02:17.839] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6] to manual commit]
[20:02:17.844] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL update]
[20:02:17.844] [DEBUG] [main] [org.springframework.jdbc.core.JdbcTemplate] [Executing prepared SQL statement [update t_book set bookname = ? where bookid = ?]]
[20:02:17.849] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Initiating transaction rollback]
[20:02:17.849] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6]]
[20:02:17.855] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@a4ca3f6] after transaction]
[20:02:17.856] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Resuming suspended transaction after completion of inner transaction]
[20:02:17.857] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Initiating transaction rollback]
[20:02:17.857] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd]]
[20:02:17.858] [DEBUG] [main] [org.springframework.jdbc.datasource.DataSourceTransactionManager] [Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@56aaaecd] after transaction]

总结:
如果在事务提交前未发生异常,则修改成功;如果在事务提交之前有异常发生,事务回滚。注意:这里是执行一条SQL语句,事务提交一次。

注意:不能用try-catch处理异常,如果使用了try-catch,不管有没有捕获到异常,总事务总是提交。

4.4 基于XML的声明式事务

4.4.1 在pom.xml文件中加入依赖

	 <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.3.1</version>
        </dependency>

4.4.2 将之前的注解全部去掉,并且修改Spring-conf.xml的配置文件

	<aop:config>
        <!-- 配置切入点表达式,将事务功能定位到具体方法上 -->
        <aop:pointcut id="txPoint" expression="execution(* com.zhang..*Service.*(..))"/>
        <!-- 将事务通知和切入点表达式关联起来 -->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPoint"/>
    </aop:config>

    <!-- tx:advice标签:配置事务通知 -->
    <!-- id属性:给事务通知标签设置唯一标识,便于引用 -->
    <!-- transaction-manager属性:关联事务管理器 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- tx:method标签:配置具体的事务方法 -->
            <!-- name属性:指定方法名,可以使用星号代表多个字符 -->
            <tx:method name="query*" read-only="true"/>
            <!-- read-only属性:设置只读属性 -->
            <!-- rollback-for属性:设置回滚的异常 -->
            <!-- no-rollback-for属性:设置不回滚的异常 -->
            <!-- isolation属性:设置事务的隔离级别 -->
            <!-- timeout属性:设置事务的超时属性 -->
            <!-- propagation属性:设置事务的传播行为 -->
            <tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
            <tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
            <tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        </tx:attributes>
    </tx:advice>

4.4.3 测试

	@Override
    public Integer updateName(Book book) {
        int update = bookDao.updateName(book);
        System.out.println(10/0);
        return update;
    }

结果:它没有发生回滚。说明设置事务成功!

  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

☞^O^☜♞

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值