spring2021

1.Spring简介

1.1.公司介绍

官网地址:https://spring.io/

1.2.Spring旗下的众多项目

From configuration to security, web apps to big data—whatever the infrastructure needs of your application may be, there is a Spring Project to help you build it. Start small and use just what you need—Spring is modular by design.

项目列表:https://spring.io/projects

1.3.Spring Framework

Spring 基础框架,可以视为 Spring 基础设施,基本上任何其他 Spring 项目都是以 Spring Framework 为基础的。

1.3.1.Spring Framework优良特性

  1. 非侵入式:使用 Spring Framework 开发应用程序时,Spring 对应用程序本身的结构影响非常小。对领域模型可以做到零污染;

    对功能性组件也只需要使用几个简单的注解进行标记,完全不会破坏原有结构,反而能将组件结构进一步简化。这就使得基于 Spring Framework 开发应用程序时结构清晰、简洁优雅。

  2. 控制反转:IOC——Inversion of Control,翻转资源获取方向。把自己创建资源、向环境索取资源变成环境将资源准备好,我们享受资源注入。

  3. 面向切面编程:AOP——Aspect Oriented Programming,在不修改源代码的基础上增强代码功能

  4. 容器:Spring IOC 是一个容器,因为它包含并且管理组件对象的生命周期。组件享受到了容器化的管理,替程序员屏蔽了组件创建过程中的大量细节,极大的降低了使用门槛,大幅度提高了开发效率。

  5. 组件化:Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML 和 Java 注解组合这些对象。这使得我们可以基于一个个功能明确、边界清晰的组件有条不紊的搭建超大型复杂应用系统。

  6. 声明式:很多以前需要编写代码才能实现的功能,现在只需要声明需求即可由框架代为实现。

  7. 一站式:在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库。而且 Spring 旗下的项目已经覆盖了广泛领域,很多方面的功能性需求可以在 Spring Framework 的基础上全部使用 Spring 来实现。

1.3.2.Spring Framework五大功能模块

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

2.IOC容器

11133305465

2.1.IOC容器概念

2.1.1.普通容器

2.1.1.1.生活中的普通容器

普通容器只能用来存储,没有更多功能。

2.1.1.2.程序中的普通容器
  • 数组
  • 集合:List
  • 集合:Set

2.1.2.复杂容器

2.1.2.1.生活中的复杂容器

政府管理我们的一生,生老病死都和政府有关。

2.1.2.2.程序中的复杂容器

Servlet 容器能够管理 Servlet、Filter、Listener 这样的组件的一生,所以它是一个复杂容器。我们即将要学习的 IOC 容器也是一个复杂容器。它们不仅要负责创建组件的对象、存储组件的对象,还要负责调用组件的方法让它们工作,最终在特定情况下销毁组件。

  1. Servlet生命周期

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

    生命周期阶段执行时机执行次数
    创建对象Web应用启动时一次
    初始化创建对象后一次
    拦截请求接收到匹配的请求多次
    销毁Web应用卸载前一次

2.1.3.IOC思想

IOC:Inversion of Control,翻译过来是反转控制

这里所指的反转是指对于传统的通过new获取资源的方式进行反转操作。

2.1.3.1.获取资源的传统方式

在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的模式下开发人员往往需要知道在具体容器中特定资源的获取方式,增加了学习成本,同时降低了开发效率。

例如在Mybatis框架中获取SqlSessionFactory、SqlSession对象

自己做饭:买菜、洗菜、择菜、改刀、炒菜,全过程参与,费时费力,必须清楚了解资源创建整个过程中的全部细节且熟练掌握。

2.1.3.2.反转控制方式获取资源

反转控制的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向——改由容器主动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可,极大的降低了学习成本,提高了开发的效率。这种行为也称为查找的被动形式。

点外卖:下单、等、吃,省时省力,不必关心资源创建过程的所有细节。

2.1.3.3.DI

DI:Dependency Injection,翻译过来是依赖注入

DI是IOC的另一种表述方式:即组件以一些预先定义好的方式(例如:setter方法)接受来自于容器的资源注入。相对于IOC而言,这种表述更直接。

所以结论是:IOC就是一种反转控制的思想, 而DI是对IOC的一种具体实现。

2.1.4.IOC容器在Spring中的实现

Spring 的 IOC 容器就是 IOC 思想的一个落地的产品实现。IOC 容器中管理的组件也叫做 bean。在创建 bean 之前,首先需要创建 IOC 容器。Spring 提供了 IOC 容器的两种实现方式:

2.1.4.1.BeanFactory

这是 IOC 容器的基本实现,是 Spring 内部使用的接口。面向 Spring 本身,不提供给开发人员使用。

2.1.4.2.ApplicationContext

ApplicationContext是BeanFactory 的子接口,提供了更多高级特性。面向 Spring 的使用者,几乎所有场合都使用 ApplicationContext 而不是底层的 BeanFactory。

以后在 Spring 环境下看到一个类或接口的名称中包含 ApplicationContext,那基本就可以断定,这个类或接口与 IOC 容器有关。

2.1.4.3.ApplicationContext的主要实现类
类型名简介
ClassPathXmlApplicationContext通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象
FileSystemXmlApplicationContext通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象
ConfigurableApplicationContextApplicationContext 的子接口,包含一些扩展方法 refresh() 和 close() ,让 ApplicationContext 具有启动、关闭和刷新上下文的能力。
WebApplicationContext专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。

2.2.基于XML管理bean

11133500887

2.2.1.创建bean

2.2.1.1.实验目标和思路
  1. 目标

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

  2. 思路

2.2.1.2.创建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>
2.2.1.3.创建组件类
package com.stonebridge.domain;

public class HappyComponent {
    public void doWork() {
        System.out.println("component do work ...");
    }
}
2.2.1.4.创建Spring配置文件
2.2.1.5.在Spring的配置文件中配置组件
<bean id="happyComponent" class="com.stonebridge.domain.HappyComponent"/>

bean标签:通过配置bean标签告诉IOC容器需要创建对象的组件是什么

id属性:bean的唯一标识

class属性:组件类的全类名

2.2.1.6.创建测试类
public class IocTest {
    // 创建IOC容器对象
    // 为了各个实验测试方法方便使用这个对象,声明成成员变量
    private ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

    @Test
    public void testExperiment01() {
        HappyComponent component = (HappyComponent) context.getBean("happyComponent");
        component.doWork();
    }
}
2.2.1.7.无参构造器的重要性
package com.stonebridge.domain;

public class HappyComponent {
    private String name;

    public HappyComponent(String name) {
        this.name = name;
    }

    public void doWork() {
        System.out.println("component do work ...");
    }
}

Spring 底层默认通过反射技术调用组件类的无参构造器来创建组件对象。如果没有无参构造器,则会抛出下面的异常:

所以对一个要注册到Spring的IOC容器中的JavaBean来说,无参构造器属性的getter()、setter()方法必须存在的。

无参构造器:用于构建组件对象,再使用setter方法为属性赋值,当使用自动装配(指需要其他组件)的时候也需要setter方法,除非使用@Autowired,否则不会为其进行装配;

Spring容器中如果通过setter方法注入属性,创建对象必须使用无参构造器;如果使用构造器注入属性时,必须使用有参构造器,无参构造器非必须的。

2.2.1.8.用IOC容器创建对象和自己建区别

在Spring环境下能够享受到的所有福利,都必须通过 IOC 容器附加到组件类上,所以随着我们在 Spring 中学习的功能越来越多,IOC 容器创建的组件类的对象就会比自己 new 的对象强大的越来越多。

2.2.2.获取bean

2.2.2.1.根据id获取

由于 id 属性指定了 bean 的唯一标识,所以根据 bean 标签的 id 属性可以精确获取到一个组件对象。上个实验中我们使用的就是这种方式。

@Test
public void testExperiment01() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    HappyComponent component = (HappyComponent) context.getBean("happyComponent");
    System.out.println("component=" + component);
    component.doWork();
}
2.2.2.2.根据类型获取
  1. 指定类型的bean唯一

    根据类型获取

    @Test
    public void testExperiment02() {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        HappyComponent component = context.getBean(HappyComponent.class);
        System.out.println("component=" + component);
        component.doWork();
    }
    
  2. 指定类型的 bean 不唯一

    <bean id="happyComponent1" class="com.stonebridge.domain.HappyComponent"/>
    <bean id="happyComponent2" class="com.stonebridge.domain.HappyComponent"/>
    

    相同类型的 bean 在IOC容器中一共配置了两个:

  3. 思考

    • 如果组件类实现了接口,根据接口类型可以获取 bean 吗?

      可以,前提是bean唯一

    • 如果一个接口有多个实现类,这些实现类都配置了 bean,根据接口类型可以获取 bean 吗?

      不行,因为bean不唯一

    如果IOC容器中接口类型的bean只有一个,那么这个接口类型的bean对象对接口类型执行instanceof判断能够返回true,那就能够获取到,这是面向对象多态性的一个体现。

    所以根据类型获取bean的依据其实就是“对象 instanceof 指定的类型”,返回true就能获取到,如果IOC容器中接口类型的bean不止一个,那还是会抛出NoUniqueBeanDefinitionException异常。

  4. 结论

    根据类型来获取bean时,在满足bean唯一性的前提下,其实只是看:『对象 instanceof 指定的类型』的返回结果,只要返回的是true就可以认定为和类型匹配(例如在接口类接受通过实现类类型去获取对象),能够获取到。

2.2.3.给bean的属性赋值:setter注入

2.2.3.1.给组件类添加一个属性
public class HappyComponent {
    private String name;

    public String getName() {
        return name;
    }

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

    public HappyComponent() {
    }

}
2.2.3.2.在配置时给属性指定值

通过property标签配置的属性值会通过setter()方法注入,setter方法是必须的;使用构造器注入可以不需要。

<!-- 实验三 [重要]给bean的属性赋值:setter注入 -->
<bean id="happyComponent3" class="com.stonebridge.domain.HappyComponent">

    <!-- property标签:通过组件类的setXxx()方法给组件对象设置属性 -->
    <!-- name属性:指定属性名(这个属性名是getXxx()、setXxx()方法定义的,和成员变量无关) -->
    <!-- value属性:指定属性值 -->
    <property name="name" value="i am very happy"/>
</bean>
2.2.3.3.测试
@Test
public void testExperiment03() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    HappyComponent happyComponent3 = (HappyComponent) context.getBean("happyComponent3");
    String happyName = happyComponent3.getName();
    System.out.println("happyName = " + happyName);
}

2.2.4.给bean的属性赋值:引用外部已声明的bean

2.2.4.1.声明新的组件类

自带无参构造器

public class HappyMachine {
    private String machineName;

    public String getMachineName() {
        return machineName;
    }

    public void setMachineName(String machineName) {
        this.machineName = machineName;
    }
}
2.2.4.2.原组件引用新组件

自带无参构造器

public class HappyComponent {
    private String name;
    private HappyMachine machine;

    public String getName() {
        return name;
    }

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

    public HappyMachine getMachine() {
        return machine;
    }

    public void setMachine(HappyMachine machine) {
        this.machine = machine;
    }
}
2.2.4.3.配置bean

配置新组件的 bean,在原组件的 bean 中引用新组件的 bean

<bean id="happyMachine" class="com.stonebridge.domain.HappyMachine">
    <property name="machineName" value="makeHappy"/>
</bean>
<bean id="happyComponent4" class="com.stonebridge.domain.HappyComponent">
    <!-- 使用外部已经配置好的happyMachine这个bean来给当前组件的happyMachine属性赋值 -->
    <!-- 引用另外一个bean不能再使用value属性,而要使用ref属性指定bean的id -->
    <!--
            如果错把ref属性写成了value属性,会抛出异常:
            Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'com.atguigu.ioc.component.HappyMachine' for property 'happyMachine': no matching editors or conversion strategy found
            意思是不能把String类型转换成我们要的HappyMachine类型
            说明我们使用value属性时,Spring只把这个属性看做一个普通的字符串,不会认为这是一个bean的id,更不会根据它去找到bean来赋值
        -->
    <property name="machine" ref="happyMachine"/>
</bean>
2.2.4.4.测试
@Test
public void testExperiment04() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    HappyComponent happyComponent4 = (HappyComponent) context.getBean("happyComponent4");
    HappyMachine happyMachine = happyComponent4.getMachine();
    String machineName = happyMachine.getMachineName();
    System.out.println("machineName = " + machineName);
}
2.2.4.5.易错点

如果错把ref属性写成了value属性,会抛出异常: Caused by: java.lang.IllegalStateException: Cannot convert value of type ‘java.lang.String’ to required type ‘com.atguigu.ioc.component.HappyMachine’ for property ‘happyMachine’: no matching editors or conversion strategy found 意思是不能把String类型转换成我们要的HappyMachine类型 说明我们使用value属性时,Spring只把这个属性看做一个普通的字符串,不会认为这是一个bean的id,更不会根据它去找到bean来赋值

2.2.5.给bean的属性赋值:内部bean

2.2.5.1.重新配置原组件

在bean里面配置的bean就是内部bean,内部bean只能在当前bean内部使用,在其他地方不能使用。

<bean id="happyComponent5" class="com.stonebridge.domain.HappyComponent">
    <property name="machine">
        <!-- 在property标签内部再配置一个bean,这就是内部bean -->
        <!-- 内部bean可以直接用来给property指定的属性赋值 -->
        <!-- 由于内部bean仅限于在bean内部使用,所以可以不配置id属性 -->
        <bean class="com.stonebridge.domain.HappyMachine">
            <property name="machineName" value="makeHappy"/>
        </bean>
    </property>
</bean>
2.2.5.2.测试
@Test
public void testExperiment05() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    HappyComponent happyComponent5 = (HappyComponent) context.getBean("happyComponent5");
    HappyMachine happyMachine = happyComponent5.getMachine();
    String machineName = happyMachine.getMachineName();
    System.out.println("machineName = " + machineName);
}

2.2.6.给bean的属性赋值:引入外部属性文件

2.2.6.1.引入数据库相关依赖
<!-- MySQL驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.3</version>
</dependency>
<!-- 数据源 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.31</version>
</dependency>
2.2.6.2.创建外部属性文件
jdbc.user=root
jdbc.password=123456
jdbc.url=jdbc:mysql://localhost:3306/mybatis-example?useUnicode=true&characterEncoding=UTF-8
jdbc.driver=com.mysql.jdbc.Driver
2.2.6.3.配置Bean
<!-- 引入外部属性文件 -->
<!-- 使用context名称空间下的property-placeholder标签引入外部属性文件(本质上就是记录外部属性文件的位置) -->
<!-- location属性:指定外部属性文件的路径。classpath:表示这个路径从类路径根目录开始 -->
<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>

注意引入命名空间

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" 
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
</beans>
2.2.6.4.测试
@Test
public void testExperiment06() throws SQLException {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    DataSource dataSource = context.getBean(DataSource.class);
    Connection connection = dataSource.getConnection();
    System.out.println("connection = " + connection);
}

2.2.7.给bean的属性赋值:级联属性赋值

2.2.7.1.装配关联对象并赋值级联属性

关联对象:happyMachine

级联属性:happyMachine.machineName

<!-- 实验七 给bean的属性赋值:级联属性赋值 -->
<bean id="happyComponent7" class="com.stonebridge.domain.HappyComponent">
    <!-- 使用内部bean的形式将happyMachine属性初始化 -->
    <property name="machine">
        <!-- 相当于创建了HappyMachine的空对象赋值给happyMachine属性 -->
        <bean class="com.stonebridge.domain.HappyMachine"/>
    </property>
    <!--
            如果happyMachine属性没有被初始化,那么直接访问级联属性会抛出异常:
            Caused by: org.springframework.beans.NullValueInNestedPathException: Invalid property 'happyMachine' of bean class [com.atguigu.ioc.component.HappyComponent]: Value of nested property 'happyMachine' is null
        -->
    <property name="machine.machineName" value="aaa"/>
</bean>
2.2.7.2.测试
@Test
public void testExperiment07() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    HappyComponent happyComponent7 = (HappyComponent) context.getBean("happyComponent7");
    String machineName = happyComponent7.getMachine().getMachineName();
    System.out.println("machineName = " + machineName);
}

2.2.8.给bean的属性赋值:构造器注入

2.2.8.1.声明组件类

Spring容器中如果通过setter方法注入属性,创建对象必须使用无参构造器;此时使用构造器注入属性时,必须使用有参构造器,无参构造器非必须的。

package com.stonebridge.domain;

public class HappyTeam {
    private String teamName;
    private Integer memberCount;
    private Double memberSalary;

    public HappyTeam(String teamName, Integer memberCount, Double memberSalary) {
        this.teamName = teamName;
        this.memberCount = memberCount;
        this.memberSalary = memberSalary;
    }


    @Override
    public String toString() {
        return "HappyTeam{" +
                "teamName='" + teamName + '\'' +
                ", memberCount=" + memberCount +
                ", memberSalary=" + memberSalary +
                '}';
    }
}
2.2.8.2.配置Bean对象
<bean id="happyTeam" class="com.stonebridge.domain.HappyTeam">
    <!-- 调用类的构造器创建对象并同时传入参数值 -->
    <!-- constructor-arg标签:给构造器对应位置传入参数 -->
    <!-- index属性:指定当前参数在参数列表中的索引位置 -->
    <!-- name属性:指定当前参数的参数名 -->
    <!-- value属性:指定参数值 -->
    <constructor-arg name="teamName" value="gaysTeam"/>
    <constructor-arg name="memberCount" value="10"/>
    <constructor-arg name="memberSalary" value="555.55"/>
</bean>
2.2.8.3.测试
@Test
public void testExperiment08() {
    HappyTeam happyTeam = iocContainer.getBean(HappyTeam.class);
    System.out.println("happyTeam = " + happyTeam);
}
2.2.8.4.补充

constructor-arg标签还有两个属性可以进一步描述构造器参数:

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

2.2.9.给bean的属性赋值:特殊值处理

2.2.9.1.声明一个类用于测试
package com.stonebridge.domain;

public class PropValue {
    private String commonValue;
    private String expression;

    public String getCommonValue() {
        return commonValue;
    }

    public void setCommonValue(String commonValue) {
        this.commonValue = commonValue;
    }

    public String getExpression() {
        return expression;
    }

    public void setExpression(String expression) {
        this.expression = expression;
    }

    @Override
    public String toString() {
        return "PropValue{" +
                "commonValue='" + commonValue + '\'' +
                ", expression='" + expression + '\'' +
                '}';
    }

    public PropValue(String commonValue, String expression) {
        this.commonValue = commonValue;
        this.expression = expression;
    }

    public PropValue() {
    }
}
2.2.9.2.字面量
  1. 用Java代码举例说明

    字面量是相对于变量来说的。看下面的代码:

    int a = 10;
    

    声明一个变量a,初始化为10,此时a就不代表字母a了,而是作为一个变量的名字。当我们引用a的时候,我们实际上拿到的值是10。

    而如果a是带引号的:‘a’,那么它现在不是一个变量,它就是代表a这个字母本身,这就是字面量。所以字面量没有引申含义,就是我们看到的这个数据本身。

  2. Spring配置文件中举例

    • 字面量举例

      <!-- 使用value属性给bean的属性赋值时,Spring会把value属性的值看做字面量 -->
      <property name="commonValue" value="hello"/>
      
    • 类似变量举例

      <!-- 使用ref属性给bean的属性复制是,Spring会把ref属性的值作为一个bean的id来处理 -->
      <!-- 此时ref属性的值就不是一个普通的字符串了,它应该是一个bean的id -->
      <property name="happyMachine" ref="happyMachine"/>
      
2.2.9.3.null值

Spring容器创建Bean对象的时候,属性默认为null,故不设置也会为null

<property name="commonValue">
    <!-- null标签:将一个属性值明确设置为null -->
    <null/>
</property>
2.2.9.4.XML实体
<!-- 实验九 给bean的属性赋值:特殊值处理 -->
<bean id="propValue1" class="com.stonebridge.domain.PropValue">
    <!-- 小于号在XML文档中用来定义标签的开始,不能随便使用 -->
    <!-- 解决方案一:使用XML实体来代替 -->
    <property name="expression" value="a &lt; b"/>
</bean>
2.2.9.5.CDATA节
<!-- 实验九 给bean的属性赋值:特殊值处理 -->
<bean id="propValue2" class="com.stonebridge.domain.PropValue">
    <property name="expression">
        <!-- 解决方案二:使用CDATA节 -->
        <!-- CDATA中的C代表Character,是文本、字符的含义,CDATA就表示纯文本数据 -->
        <!-- XML解析器看到CDATA节就知道这里是纯文本,就不会当作XML标签或属性来解析 -->
        <!-- 所以CDATA节中写什么符号都随意 -->
        <value><![CDATA[a < b]]></value>
    </property>
</bean>

2.2.10.给bean的属性赋值:使用p名称空间

2.2.10.1.配置

使用 p 名称空间的方式可以省略子标签 property,将组件属性的设置作为 bean 标签的属性来完成。

<bean id="propValue3" class="com.stonebridge.domain.PropValue" p:commonValue="commonValue11111"/>

使用 p 名称空间需要导入相关的 XML 约束,在 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:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
</beans>

具体操作时,输入p:稍微等一下,等IDEA弹出下面的提示:

按Alt+Enter即可导入。

2.2.10.2.测试
@Test
public void testExperiment10() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    PropValue propValue = (PropValue) context.getBean("propValue3");
    System.out.println("propValue = " + propValue);
}

2.2.11.给bean的属性赋值:集合属性

2.2.11.1.给组件类添加属性
package com.stonebridge.domain;

import java.util.List;
import java.util.Map;

public class HappyTeam {
    private List<String> memNameList;
    private Map<String, String> managerMap;

    public Map<String, String> getManagerMap() {
        return managerMap;
    }

    public void setManagerMap(Map<String, String> managerMap) {
        this.managerMap = managerMap;
    }

    public List<String> getMemNameList() {
        return memNameList;
    }

    public void setMemNameList(List<String> memNameList) {
        this.memNameList = memNameList;
    }

    public HappyTeam() {
    }
    
    @Override
    public String toString() {
        return "HappyTeam{" +
                "memNameList=" + memNameList +
                ", managerMap=" + managerMap +
                '}';
    }
}
2.2.11.2.配置Bean
<bean id="happyTeam3" class="com.stonebridge.domain.HappyTeam">
    <property name="memNameList">
        <list>
            <value>111a111</value>
            <value>222w222</value>
            <value>3333s33</value>
        </list>
    </property>
    <property name="managerMap">
        <!--            <map>-->
        <!--                <entry key="财务部" value="张三"></entry>-->
        <!--                <entry key="行政部" value="李四"></entry>-->
        <!--            </map>-->
        <props>
            <prop key="财务部">张三</prop>
            <prop key="行政部">李四</prop>
        </props>
    </property>
</bean>
2.2.11.3.测试
@Test
public void testExperiment11() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    HappyTeam happyTeam = (HappyTeam) context.getBean("happyTeam3");
    System.out.println("happyTeam = " + happyTeam);
}
2.2.11.4.其他变化形式
<!-- 实验十一 给bean的属性赋值:集合属性 -->
<bean id="happyTeam2" class="com.atguigu.ioc.component.HappyTeam">
    <property name="memberNameList">
        <!-- list标签:准备一组集合类型的数据,给集合属性赋值 -->
        <!--<list>
            <value>member01</value>
            <value>member02</value>
            <value>member03</value>
        </list>-->
        <!-- 使用set标签也能实现相同效果,只是附带了去重功能 -->
        <!--<set>
            <value>member01</value>
            <value>member02</value>
            <value>member02</value>
        </set>-->
        <!-- array也同样兼容 -->
        <array>
            <value>member01</value>
            <value>member02</value>
            <value>member02</value>
        </array>
    </property>
    <property name="managerList">
        <!-- 给Map类型的属性赋值 -->
        <!--<map>
            <entry key="财务部" value="张三"/>
            <entry key="行政部" value="李四"/>
            <entry key="销售部" value="王五"/>
        </map>-->
        <!-- 也可以使用props标签 -->
        <props>
            <prop key="财务部">张三2</prop>
            <prop key="行政部">李四2</prop>
            <prop key="销售部">王五2</prop>
        </props>
    </property>
</bean>

2.2.12.自动装配

2.2.12.1.声明组件类

所谓自动装配就是一个组件需要其他组件时,由 IOC 容器负责在IOC容器中找到那个需要的组件,并装配进去。

例如HappyController需要用到HappyService。IOC容器为其自动装配。

public class HappyController {
    private HappyService happyService;
    
    public HappyService getHappyService() {
        return happyService;
    }
    
    public void setHappyService(HappyService happyService) {
        this.happyService = happyService;
    }
}
public class HappyService {
}
2.2.12.2.手动装配
<bean id="happyService" class="com.stonebridge.service.HappyService"/>
<bean id="happyController" class="com.stonebridge.controller.HappyController">
    <!-- 手动装配:在property标签中使用ref属性明确指定要装配的bean -->
    <property name="happyService" ref="happyService"/>
</bean>
2.2.12.3.错误自动装配
  1. 错误1:byType表示根据类型进行装配,此时如果类型匹配的bean不止一个,那么会抛NoUniqueBeanDefinitionException

    <!-- 实验十二 自动装配 -->
    <bean id="happyService3" class="com.stonebridge.service.HappyService"/>
    <bean id="happyService2" class="com.stonebridge.service.HappyService"/>
    
    <bean id="happyController" class="com.stonebridge.controller.HappyController" autowire="byType">
    </bean>
    
  2. 错误2:byName表示根据bean的id进行匹配。而bean的id是根据需要装配组件的属性的属性名来确定的,如果不一致会无法装配到。

    <!-- 实验十二 自动装配 -->
    <bean id="happyService3" class="com.stonebridge.service.HappyService"/>
    <bean id="happyService2" class="com.stonebridge.service.HappyService"/>
    
    <bean id="happyController" class="com.stonebridge.controller.HappyController" autowire="byType">
    </bean>
    
  3. 需要装配组件的类必须要有setter方法(一般getter方法和setter方法一起出现),否则也会无法装配到。

    此处注销setter方法

    public class HappyController {
        private HappyService happyService;
    
        public HappyService getHappyService() {
            return happyService;
        }
    
    //    public void setHappyService(HappyService happyService) {
    //        this.happyService = happyService;
    //    }
    }
    
2.2.12.4.正确自动装配
<!-- 实验十二 自动装配 -->
<bean id="happyService" class="com.stonebridge.service.HappyService"/>
<bean id="happyService2" class="com.stonebridge.service.HappyService"/>

<bean id="happyController" class="com.stonebridge.controller.HappyController" autowire="byName">
</bean>
2.2.12.5.测试
@Test
public void testExperiment12() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    HappyController happyController = context.getBean(HappyController.class);
    HappyService happyService = happyController.getHappyService();
    System.out.println("happyService = " + happyService);
}

2.2.13.FactoryBean机制

2.2.13.1.简介

FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的bean不同,配置一个FactoryBean类型的bean,在获取bean的时候得到的并不是class属性中配置的这个类的对象,而是getObject()方法的返回值。通过这种机制,Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来,只把最简洁的使用界面展示给我们。

将来我们整合Mybatis时,Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的。

public interface FactoryBean<T> {
    String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";

    @Nullable
    T getObject() throws Exception;

    @Nullable
    Class<?> getObjectType();

    default boolean isSingleton() {
        return true;
    }
}
2.2.13.2.实现FactoryBean接口
public class HappyFactoryBean implements FactoryBean<HappyMachine> {
    private String machineName;

    public String getMachineName() {
        return machineName;
    }

    public void setMachineName(String machineName) {
        this.machineName = machineName;
    }

    @Override
    public HappyMachine getObject() throws Exception {
        // 方法内部模拟创建、设置一个对象的复杂过程
        HappyMachine happyMachine = new HappyMachine();
        happyMachine.setMachineName(this.machineName);
        return happyMachine;
    }

    @Override
    public Class<?> getObjectType() {
        // 返回要生产的对象的类型
        return HappyMachine.class;
    }
}
2.2.13.3.配置bean
<!-- 实验十四 FactoryBean机制 -->
<!-- 这个bean标签中class属性指定的是HappyFactoryBean,但是将来从这里获取的bean是HappyMachine对象 -->
<bean id="happyMachine3" class="com.stonebridge.factory.HappyFactoryBean">
    <!-- property标签仍然可以用来通过setXxx()方法给属性赋值 -->
    <property name="machineName" value="iceCreamMachine"/>
</bean>
2.2.13.4.测试获取bean
  • 配置的bean:HappyFactoryBean
  • 获取bean后得到的bean:HappyMachine
@Test
public void testExperiment14() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    HappyMachine happyMachine3 = (HappyMachine) context.getBean("happyMachine3");
    String machineName = happyMachine3.getMachineName();
    System.out.println("machineName = " + machineName);
}

2.2.14.bean的作用域

2.2.14.1.概念

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

取值含义创建对象的时机
singleton在IOC容器中,这个bean的对象始终为单实例IOC容器初始化时
prototype这个bean在IOC容器中有多个实例获取bean时

如果是在WebApplicationContext环境下还会有另外两个作用域(但不常用):

取值含义
request在一个请求范围内有效
session在一个会话范围内有效
2.2.14.2.配置

设置单例

<!-- 实验十五 bean的作用域 -->
<!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象 -->
<!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 -->
<bean id="happyMachine4" class="com.stonebridge.domain.HappyMachine">
    <property name="machineName" value="iceCreamMachine"/>
</bean>
2.2.14.3.测试
@Test
public void testExperiment15() {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    HappyMachine happyMachine01 = (HappyMachine) context.getBean("happyMachine4");
    HappyMachine happyMachine02 = (HappyMachine) context.getBean("happyMachine4");
    System.out.println(happyMachine01 == happyMachine02);
    System.out.println("happyMachine01.hashCode() = " + happyMachine01.hashCode());
    System.out.println("happyMachine02.hashCode() = " + happyMachine02.hashCode());
}
2.2.14.4.设置多例
<bean id="happyMachine4" scope="prototype" class="com.stonebridge.domain.HappyMachine">
    <property name="machineName" value="iceCreamMachine"/>
</bean>

2.2.15.bean的生命周期

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

    用com.stonebridge.domain.HappyComponent类测试:

    public void happyInitMethod() {
        System.out.println("HappyComponent初始化");
    }
    
    public void happyDestroyMethod() {
        System.out.println("HappyComponent销毁");
    }
    
  2. 配置bean时指定初始化和销毁方法

    <!-- 实验十六 bean的生命周期 -->
    <!-- 使用init-method属性指定初始化方法 -->
    <!-- 使用destroy-method属性指定销毁方法 -->
    <bean id="happyComponent" class="com.stonebridge.domain.HappyComponent"
          init-method="happyInitMethod"
          destroy-method="happyDestroyMethod">
        <property name="name" value="uuu"/>
    </bean>
    
    
2.2.15.3.bean的后置处理器

初始化操作之前和之后进行的操作

  1. 创建后置处理器类

    public class MyHappyBeanProcessor 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;
        }
    }
    
  2. 把bean的后置处理器放入IOC容器

    <!-- bean的后置处理器要放入IOC容器才能生效 -->
    <bean id="myHappyBeanProcessor" class="com.stonebridge.process.MyHappyBeanProcessor"/>
    
  3. 执行效果示例

2.3.基于注解管理bean

11133834309

2.3.1.注解的作用

2.3.1.1.注解

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

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

举例:元旦联欢会要布置教室,蓝色的地方贴上元旦快乐四个字,红色的地方贴上拉花,黄色的地方贴上气球。

班长做了所有标记,同学们来完成具体工作。墙上的标记相当于我们在代码中使用的注解,后面同学们做的工作,相当于框架的具体操作。

2.3.1.2.扫描

Spring 为了知道程序员在哪些地方标记了什么注解,就需要通过扫描的方式,来进行检测。然后根据注解进行后续操作。

2.3.1.3.新建Module
  1. 引人依赖

    <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>
    
  2. 创建Spring配置文件

    2.3.1.4.创建一组组件类
    1. 使用@Component注解标记的普通组件

      package com.stonebridge.component;
      
      import org.springframework.stereotype.Component;
      
      @Component
      public class CommonComponent {
          
      }
      
    2. 使用@Controller注解标记的控制器组件

      这个组件就是我们在三层架构中表述层里面,使用的控制器。以前是Servlet,以后我们将会使用Controller来代替Servlet。

      package com.stonebridge.component;
      
      import org.springframework.stereotype.Controller;
      
      @Controller
      public class SoldierController {
          
      }
      
    3. 使用@Service注解标记的业务逻辑组件

      package com.stonebridge.component;
      
      import org.springframework.stereotype.Service;
      
      @Service
      public class SoldierService {
          
      }
      
    4. 使用@Repository注解标记的持久化层组件

      这个组件就是我们以前用的Dao类,但是以后我们整合了Mybatis,这里就变成了Mapper接口,而Mapper接口是由Mybatis和Spring的整合包负责扫描的。

      由于Mybatis整合包想要把Mapper接口背后的代理类加入Spring的IOC容器需要结合Mybatis对Mapper配置文件的解析,所以这个事情是Mybatis和Spring的整合包来完成,将来由Mybatis负责扫描,也不使用@Repository注解。

      @Repository
      public class SoldierDao {
          
      }
      
2.3.1.4.四个典型注解没有本质区别

通过查看源码我们得知,@Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。

对于Spring使用IOC容器管理这些组件来说没有区别。所以@Controller、@Service、@Repository这三个注解只是给开发人员看的,让我们能够便于分辨组件的作用。

注意:虽然它们本质上一样,但是为了代码的可读性,为了程序结构严谨我们肯定不能随便胡乱标记。

2.3.1.5.扫描注解
  1. 最基本的扫描方式[常用]

    <!-- 最基本的扫描方式 -->
    <context:component-scan base-package="com.stonebridge.component"/>
    

    从IOC容器中获取bean:

    @Test
    public void testAnnotationcScanBean() {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        CommonComponent commonComponent = context.getBean(CommonComponent.class);
        SoldierController soldierController = context.getBean(SoldierController.class);
        SoldierService soldierService = context.getBean(SoldierService.class);
        SoldierDao soldierDao = context.getBean(SoldierDao.class);
        System.out.println("commonComponent = " + commonComponent);
        System.out.println("soldierController = " + soldierController);
        System.out.println("soldierService = " + soldierService);
        System.out.println("soldierDao = " + soldierDao);
    }
    
  2. 指定匹配模式

    <context:component-scan base-package="com.stonebridge.component" resource-pattern="Soldier*.class"/>
    
  3. 指定要排除的组件

    或者也可以说指定不扫描的组件

    context:exclude-filter标签:指定排除规则

    type属性:指定根据什么来进行排除,annotation取值表示根据注解来排除

    expression属性:指定排除规则的表达式,对于注解来说指定全类名即可

    <context:component-scan base-package="com.stonebridge.component">
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    
  4. 仅扫描指定组件

    仅扫描 = 关闭默认规则 + 追加规则

    use-default-filters属性:取值false表示关闭默认扫描规则

    context:include-filter标签:指定在原有扫描规则的基础上追加的规则

    <!-- 情况四:仅扫描指定的组件 -->
    <!-- 仅扫描 = 关闭默认规则 + 追加规则 -->
    <!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
    <context:component-scan base-package="com.stonebridge.component" use-default-filters="false">
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
    </context:component-scan>
    
2.3.1.6.组件的beanName

在我们使用XML方式管理bean的时候,每个bean都有一个唯一标识,便于在其他地方引用。现在使用注解后,每个组件仍然应该有一个唯一标识。

  1. 默认情况

    类名首字母小写就是bean的id。例如:SoldierController类对应的bean的id就是soldierController。

  2. 使用value属性指定

    @Controller(value = "controller")
    public class SoldierController {
        
    }
    
  3. 当注解中只设置一个属性时,value属性的属性名可以省略:

    @Service("service")
    public class SoldierService {
    
    }
    

2.3.2.自动装配

2.3.2.1.设定情景

SoldierController需要SoldierService

SoldierService需要SoldierDao

同时在各个组件中声明要调用的方法。

  1. 在SoldierController中声明方法

    @Controller
    public class SoldierController {
        private SoldierService soldierService;
    
        public void getMessage() {
            soldierService.getMessage();
        }
    }
    
  2. 在SoldierService中声明方法

    @Service
    public class SoldierService {
        private SoldierDao soldierDao;
        public void getMessage() {
            soldierDao.getMessage();
        }
    }
    
  3. 在SoldierDao中声明方法

    @Repository
    public class SoldierDao {
        public void getMessage() {
            System.out.println("I am a soldier");
        }
    }
    
2.3.2.2.自动装配的实现
  1. 前提

    参与自动装配的组件(需要装配组件、被别组件装配)全部都必须在IOC容器中。

  2. @Autowired注解

    在成员变量上直接标记@Autowired注解即可,不需要提供setXxx()方法。

    以后我们在项目中的正式用法就是这样。

  3. 给Controller装配Service

    @Controller
    public class SoldierController {
        @Autowired
        private SoldierService soldierService;
    
        public void getMessage() {
            soldierService.getMessage();
        }
    }
    
  4. 给Service装配Dao

    @Service
    public class SoldierService {
        @Autowired
        private SoldierDao soldierDao;
    
        public void getMessage() {
            soldierDao.getMessage();
        }
    }
    
  5. 在SoldierDao中声明方法

    @Repository
    public class SoldierDao {
        public void getMessage() {
            System.out.println("I am a soldier");
        }
    }
    
2.3.2.3.@Autowired注解其他细节
  1. 标记在其他位置

    • 构造器

      @Controller
      public class SoldierController {
      
          private SoldierService soldierService;
      
          @Autowired
          public SoldierController(SoldierService soldierService) {
              this.soldierService = soldierService;
          }
      
          public void getMessage() {
              soldierService.getMessage();
          }
      }
      
    • setter()方法

      @Service
      public class SoldierService {
      
          private SoldierDao soldierDao;
      
          @Autowired
          public void setSoldierDao(SoldierDao soldierDao) {
              this.soldierDao = soldierDao;
          }
      
          public void getMessage() {
              soldierDao.getMessage();
          }
      }
      
2.3.2.4.@Autowired工作流程
  1. @Autowired:根据属性类型进行自动装配

    • 第一步 把 service 和dao对象创建,在service和 dao 类添加创建对象注解
    • 第二步 在 service 注入dao 对象,在 service 类添加 dao 类型属性,在属性上面使用注解

    @Autowired注入属性不需要setter方法,根据属性的类型进行自动注入

    @Service
    public class UserService {
        //定义dao 类型属性
        //不需要添加 set 方法
        //添加注入属性注解
        @Autowired
        private UserDao userDao;
        public void add() {
            System.out.println("service add.......");
            userDao.add();
        }
    }
    
  2. @Qualifier:根据名称进行注入

    @Qualifier注解的使用,和@Autowired一起配合使用,@Qualifier中写bean的名称。适用情况:如下的UserDao接口有多个实现类userDaoImpl1,userDaoImpl2,……,此时就可以使用@Qualifier进行注入

    //定义 dao 类型属性
    //不需要添加 set 方法
    //添加注入属性注解
    @Autowired //根据类型进行注入
    @Qualifier(value = "userDaoImpl1") //根据名称进行注入
    private UserDao userDao;
    
  3. @Resource:可以根据类型注入,可以根据名称注入

    @Resource属于javax包,不属于spring框架的注解。建议使用@Autowired和@Qualifier

    //@Resource //根据类型进行注入
    @Resource(name = "userDaoImpl1") //根据名称进行注入
    private UserDao userDao;
    
  4. @Value:注入普通类型属性

    @Value(value = "abc")
    private String name;
    

2.3.3.完全注解开发

体验完全注解开发,是为了给将来学习SpringBoot打基础。因为在SpringBoot中,就是完全舍弃XML配置文件,全面使用注解来完成主要的配置。

2.3.3.1.使用配置类取代配置文件
  1. 创建配置类

    package com.stonebridge.configuration;
    
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class MyConfiguration {
        
    }
    
  2. 根据配置类创建IOC容器对象

    // ClassPathXmlApplicationContext根据XML配置文件创建IOC容器对象
    private ApplicationContext iocContainer = new ClassPathXmlApplicationContext("applicationContext.xml");
    
    // AnnotationConfigApplicationContext根据配置类创建IOC容器对象
    private ApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
    
2.3.3.2.在配置类中配置bean

使用@Bean注解

@Configuration
public class MyConfiguration {
    // @Bean注解相当于XML配置文件中的bean标签
    // @Bean注解标记的方法的返回值会被放入IOC容器
    @Bean
    public CommonComponent getComponent() {
        CommonComponent commonComponent = new CommonComponent();
        commonComponent.setComponentName("created by annotation config");
        return commonComponent;
    }
}
2.3.3.3.在配置类中配置自动扫描的包
@Configuration
@ComponentScan("com.stonebridge.component")
public class MyConfiguration {
    ……
}

2.4.整合Junit4

2.4.1.整合的好处

  1. 不需要自己创建IOC容器对象了
  2. 任何需要的bean都可以在测试类中直接享受自动装配

2.4.2.实现整合

2.4.2.1.加入依赖
<!-- Spring的测试包 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.1</version>
</dependency>
2.4.2.2.创建测试类
// junit的@RunWith注解:指定Spring为Junit提供的运行器
@RunWith(SpringJUnit4ClassRunner.class)
// Spring的@ContextConfiguration指定Spring配置文件的位置
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class AnnotationTest {

    @Autowired
    private SoldierController soldierController;

    @Test
    public void testIntegration() {
        System.out.println("soldierController = " + soldierController);
    }
}

3.AOP面向切面编程

3.1.引出问题

3.1.1.声明接口

public interface Calculate {

    int add(int i, int j);

    int sub(int i, int j);

    int mul(int i, int j);

    int div(int i, int j);
}

3.1.2.给接口声明一个纯净版实现

没有额外功能

public class CalculatorPureImpl implements Calculate {
    @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.1.3.声明一个带日志功能的实现

public class CalculatorPureImpl implements Calculate {
    @Override
    public int add(int i, int j) {
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
        int result = i + j;
        System.out.println("方法内部 result = " + result);
        System.out.println("[日志] add 方法结束了,结果是:" + result);
        return result;
    }

    @Override
    public int sub(int i, int j) {
        System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
        int result = i - j;
        System.out.println("方法内部 result = " + result);
        System.out.println("[日志] sub 方法结束了,结果是:" + result);
        return result;
    }

    @Override
    public int mul(int i, int j) {
        System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);
        int result = i * j;
        System.out.println("方法内部 result = " + result);
        System.out.println("[日志] mul 方法结束了,结果是:" + result);
        return result;
    }

    @Override
    public int div(int i, int j) {
        System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);
        int result = i / j;
        System.out.println("方法内部 result = " + result);
        System.out.println("[日志] div 方法结束了,结果是:" + result);
        return result;
    }
}

3.1.4.提出问题

3.1.4.1.现有代码缺陷

针对带日志功能的实现类,我们发现有如下缺陷:

  1. 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
  2. 附加功能分散在各个业务功能方法中,不利于统一维护
3.1.4.2.解决思路

解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。

3.1.4.3.困难

解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。

3.2.代理模式

3.2.1.概念

3.2.1.1.介绍

二十三种设计模式中的一种,属于结构型模式。

它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦

调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

未使用代理:

使用代理:

3.2.1.2.生活中的代理
  1. 广告商找大明星拍广告需要经过经纪人
  2. 合作伙伴找大老板谈合作要约见面时间需要经过秘书
  3. 房产中介是买卖双方的代理
3.2.1.3.相关术语

代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。

目标:被代理“套用”了非核心逻辑代码的类、对象、方法。

理解代理模式、AOP的核心关键词就一个词:嵌套

3.2.2.静态代理(不具有实用性)

创建静态代理类:

public class CalculatorStaticProxy implements Calculate {
    // 将被代理的目标对象声明为成员变量
    private Calculate target;

    public CalculatorStaticProxy(Calculate 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.3.动态代理

3.3.1.生产代理对象的工厂类

JDK本身就支持动态代理,这是反射技术的一部分。下面我们还是创建一个代理类(生产代理对象的工厂类):

public class LogDynamicProxyFactory<T> {
    private T target;

    public LogDynamicProxyFactory(T target) {
        this.target = target;
    }

    public T getProxy() {
        //1.创建代理对象所需参数一:加载目标对象的类的类加载器
        ClassLoader classLoader = target.getClass().getClassLoader();
        //2.创建代理对象所需参数二:目标对象的类所实现的所有接口组成的数组
        Class<?>[] interfaces = target.getClass().getInterfaces();
        //3.创建代理对象所需参数三:InvocationHandler接口
        //lambda表示式口诀
        //1.复制小括号
        //2.写死右箭头
        //3.落地大括号
        /**
         * Object代理对象
         * Method代表目标方法的Method对象
         */
        InvocationHandler handler = (Object proxy, Method method, Object[] args) -> {
            //对InvocationHandler接口中invoke()方法的实现就是在调用目标方法
            //围绕目标方法的调用,添加我们的附加功能
            Object targetMethodReturnValue = null;
            // 通过method对象获取方法名
            String methodName = method.getName();
            // 为了便于在打印时看到数组中的数据,把参数数组转换为List
            List<Object> argumentList = Arrays.asList(args);
            try {
                // 在目标方法执行前:打印方法开始的日志
                System.out.println("[动态代理][日志] " + methodName + " 方法开始了,参数是:" + argumentList);
                //调用目标的方法:需要参数1:调用目标方法的目标对象,参数2:调用目标方法时的传入的实际参数
                //调用后返回的目标方法的返回值
                targetMethodReturnValue = method.invoke(target, args);
                // 在目标方法成功后:打印方法成功结束的日志【寿终正寝】
                System.out.println("[动态代理][日志] " + methodName + " 方法成功结束了,返回值是:" + targetMethodReturnValue);
            } catch (Exception exception) {
                // 通过e对象获取异常类型的全类名
                String exceptionName = exception.getClass().getName();
                // 通过e对象获取异常消息
                String message = exception.getMessage();
                // 在目标方法失败后:打印方法抛出异常的日志【死于非命】
                System.out.println("[动态代理][日志] " + methodName + " 方法抛异常了,异常信息是:" + exceptionName + "," + message);
            } finally {
                // 在目标方法最终结束后:打印方法最终结束的日志【盖棺定论】
                System.out.println("[动态代理][日志] " + methodName + " 方法最终结束了");
            }
            // 这里必须将目标方法的返回值返回给外界,如果没有返回,外界将无法拿到目标方法的返回值
            return targetMethodReturnValue;
        };
        
        //创建代理对象
        return (T) Proxy.newProxyInstance(classLoader, interfaces, handler);
    }
}

3.3.2.测试

@Test
public void testDynamicProxy() {
    // 1.创建被代理的目标对象
    Calculate target = new CalculatorPureImpl();
    // 2.创建能够生产代理对象的工厂对象
    LogDynamicProxyFactory<Calculate> factory = new LogDynamicProxyFactory<>(target);
    // 3.通过工厂对象生产目标对象的代理对象
    Calculate proxy = factory.getProxy();
    // 4.通过代理对象间接调用目标对象
    int addResult = proxy.add(10, 2);
    System.out.println("方法外部 addResult = " + addResult + "\n");

    int subResult = proxy.sub(10, 2);
    System.out.println("方法外部 subResult = " + subResult + "\n");

    int mulResult = proxy.mul(10, 2);
    System.out.println("方法外部 mulResult = " + mulResult + "\n");

    int divResult = proxy.div(10, 2);
    System.out.println("方法外部 divResult = " + divResult + "\n");
}

3.3.3.通用性测试

动态代理的实现过程不重要,重要的是使用现成的动态代理类去用到其他目标对象上。

声明另外一个接口:

public interface SoldierService {
    int saveSoldier(String soldierName);

    int removeSoldier(Integer soldierId);

    int updateSoldier(Integer soldierId, String soldierName);

    String getSoldierNameById(Integer soldierId);
}

给接口一个实现类:

public class SoldierServiceImpl implements SoldierService {
    @Override
    public int saveSoldier(String soldierName) {
        System.out.println("核心业务逻辑:保存到数据库……");
        return 1;
    }

    @Override
    public int removeSoldier(Integer soldierId) {
        System.out.println("核心业务逻辑:从数据库删除……");
        return 1;
    }

    @Override
    public int updateSoldier(Integer soldierId, String soldierName) {
        System.out.println("核心业务逻辑:更新……");
        return 1;
    }

    @Override
    public String getSoldierNameById(Integer soldierId) {
        System.out.println("核心业务逻辑:查询数据库……");
        return "good";
    }
}

测试:

@Test
public void testSoldierServiceDynamicProxy() {
    // 1.创建被代理的目标对象
    SoldierService soldierService = new SoldierServiceImpl();
    // 2.创建生产代理对象的工厂对象
    LogDynamicProxyFactory<SoldierService> factory = new LogDynamicProxyFactory<>(soldierService);
    // 3.生产代理对象
    SoldierService proxy = factory.getProxy();
    // 4.通过代理对象调用目标方法
    String soldierName = proxy.getSoldierNameById(1);
    System.out.println("soldierName = " + soldierName);
}

3.4.AOP的核心套路

3.5.AOP术语

3.5.1.横切关注点

从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。

就是进行通知加强的位置

3.5.2.通知[记住]

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

  • 前置通知:在被代理的目标方法执行
  • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝
  • 异常通知:在被代理的目标方法异常结束后执行(死于非命
  • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论
  • 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

3.5.3.切面[记住]

封装通知方法的类就叫切面类

3.5.4.目标

被代理的目标对象,就是原先需要被增强的类

3.5.5.代理

向目标对象应用通知之后创建的代理对象。

3.5.6.连接点

这也是一个纯逻辑概念,不是语法定义的。

把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。

3.5.7.切入点[记住]

定位连接点的方式。

每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。

如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。

Spring 的 AOP 技术可以通过切入点定位到特定的连接点。

切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

3.6.基于注解的AOP

3.6.1.AOP概念介绍

3.6.1.1.名词解释

AOP:Aspect Oriented Programming面向切面编程

3.6.1.2.AOP的作用
  • 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
  • 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。

3.6.2.基于注解的AOP用到的技术

  • 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
  • cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
  • AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。

3.6.3.AOP初步实现

3.6.3.1.新建Module

新建Module为spring-aop-annotation-proxy

3.6.3.2.引入依赖
<dependencies>
    <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- spring-aspects会帮我们传递过来aspectjweaver -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</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>
</dependencies>
3.6.3.3.准备被代理的目标资源
  1. 接口

    public interface Calculater {
    
        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. 接口的实现类

    在Spring环境下工作,所有的一切都必须放在IOC容器中。现在接口的实现类是AOP要代理的目标类,所以它也必须放入IOC容器。

    @Component
    public class CalculatorPureImpl implements Calculater {
        @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.6.3.4.创建切面类
  1. @Aspect表示这个类是一个切面类

  2. @Component注解保证这个切面类能够放入IOC容器

  3. @Before注解:声明当前方法是前置通知方法

    value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上

  4. @AfterReturning返回通知

  5. @AfterThrowing异常通知

  6. @After后置通知

@Aspect
@Component
public class LogAspect {

    // @Before注解:声明当前方法是前置通知方法
    // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
    @Before(value = "execution(public int com.stonebridge.aop.api.Calculator.add(int,int))")
    public void printLogBeforeCore() {
        System.out.println("[AOP前置通知] 方法开始了");
    }

    @AfterReturning(value = "execution(public int com.stonebridge.aop.api.Calculator.add(int,int))")
    public void printLogAfterSuccess() {
        System.out.println("[AOP返回通知] 方法成功返回了");
    }

    @AfterThrowing(value = "execution(public int com.stonebridge.aop.api.Calculator.add(int,int))")
    public void printLogAfterException() {
        System.out.println("[AOP异常通知] 方法抛异常了");
    }

    @After(value = "execution(public int com.stonebridge.aop.api.Calculator.add(int,int))")
    public void printLogFinallyEnd() {
        System.out.println("[AOP后置通知] 方法最终结束了");
    }

}
3.6.3.5.创建Spring的配置文件

<aop:aspectj-autoproxy/>:开启基于注解的AOP功能

<?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">
    <aop:aspectj-autoproxy/>
    <!-- 配置自动扫描的包 -->
    <context:component-scan base-package="com.stonebridge.aop"/>
</beans>
3.6.3.6.测试

自动装配是Calculator接口类型,其实装配的是目标类加了增强通知的类

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class AOPTest {

    @Autowired
    @Qualifier("calculatorPureImpl")
    private Calculator calculator;

    @Test
    public void testAnnotationAOP() {
        int add = calculator.add(10, 2);
        System.out.println("方法外部 add = " + add);
    }
}

打印效果如下:

3.6.3.7.通知执行顺序
  • Spring版本5.3.x以前:
    • 前置通知
    • 目标操作
    • 后置通知(最终通知,finally里的方法)
    • 返回通知或异常通知
  • Spring版本5.3.x以后:
    • 前置通知
    • 目标操作
    • 返回通知或异常通知
    • 后置通知(最终通知,finally里的方法)

3.6.4.各个通知获取细节信息

3.6.4.1.JoinPoint接口

org.aspectj.lang.JoinPoint

  • 要点1:JoinPoint接口通过getSignature()方法获取目标方法的签名
  • 要点2:通过目标方法签名对象获取方法名
  • 要点3:通过JoinPoint对象获取外界调用目标方法时传入的实参列表组成的数组

在通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入,根据JoinPoint对象就可以获取目标方法名称,实际参数列表。

@Before(value = "execution(public int com.stonebridge.aop.api.Calculator.add(int,int))")
public void printLogBeforeCore(JoinPoint joinPoint) {
    //1.通过JoinPoint对象获取目标方法签名对象
    //方法的签名:一个方法的全部声明信息
    Signature signature = joinPoint.getSignature();
    String methodName = signature.getName();
    System.out.println("methodName:" + methodName);

    Integer modifiers = signature.getModifiers();
    System.out.println("modifiers:" + modifiers);

    String declaringTypeName = signature.getDeclaringTypeName();
    System.out.println("declaringTypeName:" + declaringTypeName);

    // 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
    Object[] args = joinPoint.getArgs();
    // 4.由于数组直接打印看不到具体数据,所以转换为List集合
    List<Object> argList = Arrays.asList(args);
    System.out.println("[AOP前置通知] " + methodName + "方法开始了,参数列表:" + argList);
}

需要获取方法签名、传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参。

3.6.4.2.方法返回值

在返回通知中,通过@AfterReturning注解的returning属性获取目标方法的返回值

// 在返回通知中获取目标方法返回值分两步:
// 第一步:在@AfterReturning注解中通过returning属性设置一个名称
// 第二步:使用returning属性设置的名称在通知方法中声明一个对应的形参
@AfterReturning(
    value = "execution(public int com.stonebridge.aop.api.Calculator.*(int,int))", 
    returning = "targetMethodReturnValue")
public void printLogAfterSuccess(JoinPoint joinPoint, Object targetMethodReturnValue) {
    String methodName = joinPoint.getSignature().getName();
    System.out.println("[AOP返回通知] " + methodName + "方法成功结束了,返回值是:" + targetMethodReturnValue);
}
3.6.4.3.目标方法抛出的异常

在异常通知中,通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象

// @AfterThrowing注解标记异常通知方法
// 在异常通知中获取目标方法抛出的异常分两步:
// 第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
// 第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们
@AfterThrowing(
    value = "execution(public int com.stonebridge.aop.api.Calculator.*(int,int))", 
    throwing = "targetMethodException")
public void printLogAfterException(JoinPoint joinPoint, Throwable targetMethodException) {
    String methodName = joinPoint.getSignature().getName();
    System.out.println("[AOP异常通知] " + methodName + "方法抛异常了,异常类型是:" + targetMethodException.getClass().getName());
}

3.6.5.重用切入点表达式

3.6.5.1.声明

一般来说一个切面类要对一种类型的方法进行增加,可以通过重用切入点表达式把切入点表达式抽取到一起。

抽取的方式就是声明一个方法,该不需要不需要任何实现。使用@Pointcut注解,将切入点表达式为@Pointcut的vaue赋值,作为公共的切入点表达式。

如果在@Before、@Around等注解中使用,将有@Pointcut注解的方法名为@Before、@Around等注解的value赋值。

切入点表达式所在类不需要进Spring容器

在一处声明切入点表达式之后,其他有需要的地方引用这个切入点表达式。易于维护,一处修改,处处生效。声明方式如下:

// 切入点表达式重用
@Pointcut("execution(* *..*.add(..))")
public void declarPointCut() {}
3.6.5.2.同一个类内部引用
@Before(value = "declarPointCut()")
public void printLogBeforeCoreOperation(JoinPoint joinPoint) {
    ……
}
3.6.5.3.在不同类中引用

使用全类名即可

@Around(value = "com.stonebridge.spring.aop.aspect.LogAspect.declarPointCut()")
public Object roundAdvice(ProceedingJoinPoint joinPoint) {
    ……
}
3.6.5.4.集中管理

而作为存放切入点表达式的类,可以把整个项目中所有切入点表达式全部集中过来,便于统一管理:

package com.stonebridge.aop;
import org.aspectj.lang.annotation.Pointcut;
public class AtguiguPointCut {
    @Pointcut(value = "execution(public int com.stonebridge.aop.api.Calculator.*(int,int))")
    public void atguiguGlobalPointCut() {
    }
}

使用

@Aspect
@Component
public class LogAspect {
    @Before(value = "com.stonebridge.aop.AtguiguPointCut.atguiguGlobalPointCut()")
    public void printLogBeforeCore(JoinPoint joinPoint) {
       ……
    }
}

3.6.6.切入点表达式语法

3.6.6.1.切入点表达式的作用
3.6.6.2.语法细节
  • execution(……)是必须要写的

  • 权限修饰符和返回值类型不能分别用两个*号代替,只能用一个*代替,表示“权限修饰符”和“返回值”不限

    示例:“execution(public int com.stonebridge.aop.api.Calculator.*(int,int))”

    错误示例:“execution(* * com.stonebridge.aop.api.Calculator.*(int,int))”

    正确示例:“execution(* com.stonebridge.aop.api.Calculator.*(int,int))”

  • 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。

    示例:*.Hello匹配com.Hello,不匹配com.atguigu.Hello

  • 在包名的部分,使用“*…”表示包名任意、包的层次深度任意

  • 在类名的部分,类名部分整体用*号代替,表示类名任意

  • 在类名的部分,可以使用*号代替类名的一部分

    *Service
    

    上面例子表示匹配所有名称以Service结尾的类或接口

  • 在方法参数列表部分,使用(…)表示参数列表任意

  • 在方法参数列表部分,使用(int,…)表示参数列表以一个int类型的参数开头

  • 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的

    补充:切入点表达式中使用 int 和实际方法中 Integer 是不匹配的

  • 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符

    execution(public int *..*Service.*(.., int))
    
  • 对于execution()表达式整体可以使用三个逻辑运算符号

    • execution() || execution()表示满足两个execution()中的任何一个即可
    • execution() && execution()表示两个execution()表达式必须都满足
    • !execution()表示不满足表达式的其他方法
3.6.6.3.总结

3.6.7.环绕通知

环绕通知对应整个try…catch…finally结构,包括前面四种通知的所有功能。

通过在通知方法形参位置声明ProceedingJoinPoint类型的形参,Spring会将这个类型的对象传给我们。

通过ProceedingJoinPoint对象调用目标方法

@Aspect
@Component
public class TxAspect {
    @Around(value = "com.stonebridge.aop.AtguiguPointCut.atguiguGlobalPointCut()")
    // 通过在通知方法形参位置声明ProceedingJoinPoint类型的形参,
    // Spring会将这个类型的对象传给我们
    public Object managerTransaction(ProceedingJoinPoint joinPoint) {
        // 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
        Object[] args = joinPoint.getArgs();
        // 通过ProceedingJoinPoint对象获取目标方法的签名对象
        Signature signature = joinPoint.getSignature();
        // 通过签名对象获取目标方法的方法名
        String methodName = signature.getName();
        // 声明变量用来存储目标方法的返回值
        Object targetMethodReturnValue = null;
        try {
            // 在目标方法执行前:开启事务(模拟)
            System.out.println("[AOP 环绕通知] 开启事务,方法名:" + methodName + ",参数列表:" + Arrays.asList(args));
            // 目标方法的返回值一定要返回给外界调用者
            targetMethodReturnValue = joinPoint.proceed(args);
            // 在目标方法成功返回后:提交事务(模拟)
            System.out.println("[AOP 环绕通知] 提交事务,方法名:" + methodName + ",方法返回值:" + targetMethodReturnValue);
        } catch (Throwable e) {
            // 在目标方法抛异常后:回滚事务(模拟)
            System.out.println("[AOP 环绕通知] 回滚事务,方法名:" + methodName + ",异常:" + e.getClass().getName());
        } finally {
            // 在目标方法最终结束后:释放数据库连接
            System.out.println("[AOP 环绕通知] 释放数据库连接,方法名:" + methodName);
        }
        return targetMethodReturnValue;
    }
}

3.6.8.切面的优先级

3.6.8.1.概念

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用@Order注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低

示例:

  1. 日志增强切面

    @Aspect
    @Component
    @Order(7)
    public class LogAspect {
    ……
    }
    
  2. 事务管理切面

    @Aspect
    @Component
    @Order(6)
    public class TxAspect {
    ……
    }
    
  3. 测试结果

3.6.8.2.实际意义

实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。

此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。

3.6.9.目标类没有接口

在目标类没有实现任何接口的情况下,Spring会自动使用cglib技术实现代理。为了证明这一点,我们做下面的测试:

3.6.9.1.创建目标类

请确保这个类在自动扫描的包下

同时确保切面的切入点表达式能够覆盖到类中的方法。

@Service
public class EmployeeService {
    public int getEmpList(int i) {
        System.out.println("方法内部 com.atguigu.aop.imp.EmployeeService.getEmpList");
        return i;
    }
}
@Pointcut(value = "execution(public int com.stonebridge.aop.service.EmployeeService.*(..))")
public void servicePointCut() {
}
@Aspect
@Component
@Order(6)
public class TxAspect {
    @Around(value = "com.stonebridge.aop.AtguiguPointCut.servicePointCut()")
    public Object managerTransaction(ProceedingJoinPoint joinPoint) {
    ……
    }
}
3.6.9.2.测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:applicationContext.xml"})
public class AOPTest {

    @Autowired
    private EmployeeService service;

    @Test
    public void testAnnotationAOP() {
        int a = service.getEmpList(1);
        System.out.println(a);
    }
}
3.6.9.3.Debug查看
  1. 没有实现接口情况

  2. 有实现接口的情况

  3. 同时我们发现:Mybatis调用的Mapper接口类型的对象其实也是动态代理机制

3.6.10.小结

3.7.基于XML的AOP[了解]

3.7.1.准备工作

3.7.1.1.加入依赖

和基于注解的AOP时一样。

<dependencies>
    <!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.1</version>
    </dependency>
    <!-- spring-aspects会帮我们传递过来aspectjweaver -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</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>
</dependencies>
3.7.1.2.准备代码

把测试基于注解功能时的Java类复制到新module中,去除所有注解。

3.7.2.配置Spring配置文件

springContext.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="calculatorPure" class="com.atguigu.aop.imp.CalculatorPureImpl"/>
    <!-- 配置切面类的bean -->
    <bean id="logAspect" class="com.atguigu.aop.aspect.LogAspect"/>
    <!-- 配置AOP -->
    <aop:config>
        <!-- 配置切入点表达式 -->
        <aop:pointcut id="logPointCut" expression="execution(* *..*.*(..))"/>
        <!-- aop:aspect标签:配置切面 -->
        <!-- ref属性:关联切面类的bean -->
        <aop:aspect ref="logAspect">
            <!-- aop:before标签:配置前置通知 -->
            <!-- method属性:指定前置通知的方法名 -->
            <!-- pointcut-ref属性:引用切入点表达式 -->
            <aop:before method="printLogBeforeCore" pointcut-ref="logPointCut"/>
            <!-- aop:after-returning标签:配置返回通知 -->
            <!-- returning属性:指定通知方法中用来接收目标方法返回值的参数名 -->
            <aop:after-returning
                    method="printLogAfterCoreSuccess"
                    pointcut-ref="logPointCut"
                    returning="targetMethodReturnValue"/>
            <!-- aop:after-throwing标签:配置异常通知 -->
            <!-- throwing属性:指定通知方法中用来接收目标方法抛出异常的异常对象的参数名 -->
            <aop:after-throwing
                    method="printLogAfterCoreException"
                    pointcut-ref="logPointCut"
                    throwing="targetMethodException"/>
            <!-- aop:after标签:配置后置通知 -->
            <aop:after method="printLogCoreFinallyEnd" pointcut-ref="logPointCut"/>
            <!-- aop:around标签:配置环绕通知 -->
            <!--<aop:around method="……" pointcut-ref="logPointCut"/>-->
        </aop:aspect>
    </aop:config>
</beans>

3.7.3.测试

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:spring-context.xml"})
public class AOPTest {
    
    @Autowired
    private Calculator calculator;
    
    @Test
    public void testLogAspect() {
        int add = calculator.add(10, 2);
        System.out.println("add = " + add);
    }
}

3.8.AOP对获取bean的影响

3.8.1.根据类型获取bean

3.8.1.1.情景一

bean对应的类没有实现任何接口

根据bean本身的类型获取bean

  • 测试:IOC容器中同类型的bean只有一个

    正常获取到IOC容器中的那个bean对象

  • 测试:IOC容器中同类型的bean有多个

    会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

3.8.1.2.情景二

bean对应的类实现了接口,这个接口也只有这一个实现类

  • 测试:根据接口类型获取bean
  • 测试:根据实现类类型获取bean
  • 结论:上面两种情况其实都能够正常获取到bean,而且是同一个对象
3.8.1.3.情景三

声明一个接口

接口有多个实现类

接口所有实现类都放入IOC容器

  • 测试:根据接口类型获取bean

    会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

  • 测试:根据实现类类型获取bean

    正常

3.8.1.4.情景四

声明一个接口

接口有一个实现类

创建一个切面类,对上面接口的实现类应用通知

  • 测试:根据接口类型获取bean,正常获取

  • 测试:根据实现类类型获取bean,报NoSuchBeanDefinitionException异常

    原因分析:

    应用了切面后,真正放在IOC容器中的是代理类的对象。目标类并没有被放到IOC容器中,所以根据目标类的类型从IOC容器中是找不到的

    从内存分析的角度来说,IOC容器中引用的是代理对象,代理对象引用的是目标对象。IOC容器并没有直接引用目标对象,所以根据目标类本身在IOC容器范围内查找不到。

debug查看代理类的类型

3.8.1.5.情景五

声明一个类,没有实现接口

创建一个切面类,对上面的类应用通知

  • 测试:根据类获取bean,能获取到

3.8.2.自动装配

自动装配需先从IOC容器中获取到唯一的一个bean才能够执行装配。

所以装配能否成功和装配底层的原理,和前面测试的获取bean的机制是一致的。

3.8.2.1.情景一

目标bean对应的类没有实现任何接口

根据bean本身的类型装配这个bean

  • 测试:IOC容器中同类型的bean只有一个

    正常装配

  • 测试:IOC容器中同类型的bean有多个

    会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

3.8.2.2.情景二

目标bean对应的类实现了接口,这个接口也只有这一个实现类

  • 测试:根据接口类型装配bean

    正常

  • 测试:根据实现类类型装配bean

    正常

3.8.2.3.情景三

声明一个接口

接口有多个实现类

接口所有实现类都放入IOC容器

  • 测试:根据接口类型装配bean

    @Autowired注解会先根据类型查找,此时会找到多个符合的bean,然后根据成员变量名作为bean的id进一步筛选,如果没有id匹配的,则会抛出NoUniqueBeanDefinitionException异常,表示IOC容器中这个类型的bean有多个

  • 测试:根据类装配bean

    正常

3.8.2.4.情景四

声明一个接口

接口有一个实现类

创建一个切面类,对上面接口的实现类应用通知

  • 测试:根据接口类型装配bean

    正常

  • 测试:根据类装配bean

    此时获取不到对应的bean,所以无法装配,抛出下面的异常:

Caused by: org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named ‘fruitApple’ is expected to be of type ‘com.atguigu.bean.impl.FruitAppleImpl’ but was actually of type ‘com.sun.proxy.$Proxy15’

3.8.2.5.情景五

声明一个类

创建一个切面类,对上面的类应用通知

  • 测试:根据类装配bean

    正常

3.8.3.总结

3.8.3.1.对实现了接口的类应用切面

应用切面后的代理类也实现了接口,最终也会被放在容器中。所以根据原始实现类FruitAppleImpl类型在IOC容器查找是找不到的。

3.8.3.2.对没实现接口的类应用切面

CGLIB生成的代理类继承了目标类,放在IOC容器的是CGLIB生成的代理类,此时通过代理类类型去IOC容器查找是可以找到的。

4.JdbcTemplate

4.1.简介

为了在特定领域帮助我们简化代码,Spring 封装了很多 『Template』形式的模板类。例如:RedisTemplate、RestTemplate 等等,包括 JDBCTemplate。

4.2.准备环境

4.2.1.加入依赖

Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包,导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入。

<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>5.1.3</version>
    </dependency>
    <!-- 数据源 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.31</version>
    </dependency>
</dependencies>

4.2.2.建表SQL

CREATE DATABASE `mybatis-example`;

USE `mybatis-example`;

CREATE TABLE `t_emp`(
emp_id INT AUTO_INCREMENT,
emp_name CHAR(100),
emp_salary DOUBLE(10,5),
PRIMARY KEY(emp_id)
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `t_emp`(emp_name,emp_salary) VALUES("tom",200.33);

4.2.3.jdbc.properties

test.url=jdbc:mysql://localhost:3306/mybatis-example?useUnicode=true&characterEncoding=UTF-8
test.driver=com.mysql.jdbc.Driver
test.username=root
test.password=123456

4.2.4.Spring 配置文件

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

    <!-- 导入外部属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!-- 配置数据源 -->
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${test.url}"/>
        <property name="driverClassName" value="${test.driver}"/>
        <property name="username" value="${test.username}"/>
        <property name="password" value="${test.password}"/>
    </bean>
    <!-- 配置 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 装配数据源 -->
        <property name="dataSource" ref="druidDataSource"/>
    </bean>
</beans>

4.2.5.在测试类装配 JdbcTemplate

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:springContext.xml"})
public class JDBCTest {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void testJdbcTemplateUpdate() {
        // 1.编写 SQL 语句
        String sql = "select * from t_emp where emp_id=?";
        // 2.调用 jdbcTemplate 的方法执行查询
        List<Map<String, Object>> list = jdbcTemplate.queryForList(sql, new Object[]{8});
        System.out.println("list = " + list);
    }
}

4.3.基本用法

4.3.1.增删改操作

@Test
public void testJdbcTemplateUpdate1() {
    // 1.编写 SQL 语句。需要传参的地方写问号占位符
    String sql = "update t_emp set emp_salary=? where emp_id=?";
    // 2.调用 jdbcTemplate 的 update() 方法执行 update 语句
    int count = jdbcTemplate.update(sql, 999.99, 3);
    System.out.println("count = " + count); //1
}

4.3.2.查询返回单个值

第一个参数:sql 语句

第二个参数:返回类型

第三个参数:查询条件

@Test
public void testJdbcTemplateSelectOne() {
    // 1.编写 SQL 语句
    String sql = "select emp_name from t_emp where emp_id=?";
    // 2.调用 jdbcTemplate 的方法执行查询
    String empName = jdbcTemplate.queryForObject(sql, String.class, new Object[]{6});
    System.out.println(empName);
}

查询结果为空会报错

4.3.3.查询返回对象

第一个参数:sql 语句

第二个参数:RowMapper 是接口,针对返回不同类型数据,使用这个接口里面实现类完成 数据封装

第三个参数:查询条件

@Test
public void testJdbcTemplateQueryForEntity() {
    // 1.编写 SQL 语句
    String sql = "select emp_id,emp_name,emp_salary from t_emp where emp_id=?";
    // 2.准备 RowMapper 对象
    RowMapper<Emp> rowMapper = new BeanPropertyRowMapper<>(Emp.class);
    // 3.调用 jdbcTemplate 的方法执行查询
    Emp emp = jdbcTemplate.queryForObject(sql, rowMapper, 9);
    System.out.println("emp = " + emp);//emp = Emp(empId=4, empName=pott, empSalary=9999.9)
}

查询结果为空会报错

4.3.4.查询返回对象集合

第一个参数:sql 语句

第二个参数:RowMapper 是接口,针对返回不同类型数据,使用这个接口里面实现类完成 数据封装

第三个参数:查询条件

@Test
public void testJdbcTemplateQueryListEntity() {
    // 1.编写 SQL 语句
    String sql = "select emp_id,emp_name,emp_salary from t_emp where emp_id=?";
    // 2.准备 RowMapper 对象
    RowMapper<Emp> rowMapper = new BeanPropertyRowMapper<>(Emp.class);
    // 3.调用 jdbcTemplate 的方法执行查询
    List<Emp> empList = jdbcTemplate.query(sql, rowMapper, new Object[]{4});
    System.out.println("empList = " + empList);
}

4.3.5.查询返回List集合

第一个参数:sql 语句

第二个参数:查询条件

@Test
public void testJdbcTemplateQueryList() {
    // 1.编写 SQL 语句
    String sql = "select emp_id,emp_name,emp_salary from t_emp where emp_id=?";
    List<Map<String, Object>> empList = jdbcTemplate.queryForList(sql, new Object[]{5});
    System.out.println("empList = " + empList);
}

4.3.6.批量操作

第一个参数:sql 语句

第二个参数:List 集合,添加多条记录数据

@Test
public void testJdbcTemplatebatchAdd() {
    //批量添加测试
    List<Object[]> batchArgs = new ArrayList<>();
    Object[] o1 = {700, "Tom", 111};
    Object[] o2 = {800, "Jerry", 222.33};
    Object[] o3 = {900, "Trump", 778.0};
    batchArgs.add(o1);
    batchArgs.add(o2);
    batchArgs.add(o3);
    String sql = "insert into t_emp(emp_id,emp_name,emp_salary) values(?,?,?)";
    int[] ints = jdbcTemplate.batchUpdate(sql, batchArgs);
    System.out.println(Arrays.toString(ints));//[1, 1, 1]
}

5.声明式事务

5.1.声明式事务概念

5.1.1.编程式事务

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

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

编程式的实现方式存在缺陷:

  • 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
  • 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。

5.1.2.声明式事务

事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。

封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。

声明式事务好处:

  1. 提高开发效率
  2. 消除了冗余的代码
  3. 框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化。

所以,我们可以总结下面两个概念:

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

5.1.3.事务管理器

5.1.3.1.顶级接口
  1. Spring 5.2以前

    public interface PlatformTransactionManager {
        TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
        void commit(TransactionStatus status) throws TransactionException;
        void rollback(TransactionStatus status) throws TransactionException;
    }
    
  2. 从Spring5.2开始

    PlatformTransactionManager 接口本身没有变化,但是它继承了 TransactionManager

    /**
     * Marker interface for Spring transaction manager implementations,
     * either traditional or reactive.
     *
     * @author Juergen Hoeller
     * @since 5.2
     * @see PlatformTransactionManager
     * @see ReactiveTransactionManager
     */
    public interface TransactionManager {
    
    }
    
    public interface PlatformTransactionManager extends TransactionManager {
    
    	TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
    			throws TransactionException;
    
    	void commit(TransactionStatus status) throws TransactionException;
    
    	void rollback(TransactionStatus status) throws TransactionException;
    
    }
    

    TransactionManager接口中什么都没有,但是它还是有存在的意义——定义一个技术体系。

5.1.3.2.技术体系

JdbcTemplate使用的事务管理器是org.springframework.jdbc.datasource.DataSourceTransactionManager,将来整合 Mybatis 用的也是这个类。

DataSourceTransactionManager类中的主要方法:

DataSourceTransactionManager类中的主要方法:

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

5.2.基于注解的声明式事务

5.2.1.准备工作

5.2.1.1.新建Module
5.2.1.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>5.1.3</version>
    </dependency>
    <!-- 数据源 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.31</version>
    </dependency>
</dependencies>
5.2.1.3.jdbc.properties
jdbc.url=jdbc:mysql://localhost:3306/mybatis-example?useUnicode=true&characterEncoding=UTF-8
jdbc.driver=com.mysql.jdbc.Driver
jdbc.username=root
jdbc.password=123456
5.2.1.4.Spring 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       https://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 导入外部属性文件 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <!-- 配置数据源 -->
    <bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
    <!-- 配置 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 装配数据源 -->
        <property name="dataSource" ref="druidDataSource"/>
    </bean>
</beans>
5.2.1.5.组件EmpDao
@Repository
public class EmpDao {

    private JdbcTemplate jdbcTemplate;

    @Autowired
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    public void updateEmpNameById(Integer empId, String empName) {
        String sql = "update t_emp set emp_name=? where emp_id=?";
        jdbcTemplate.update(sql, empName, empId);
    }

    public void updateEmpSalaryById(Integer empId, Double salary) {
        String sql = "update t_emp set emp_salary=? where emp_id=?";
        jdbcTemplate.update(sql, salary, empId);
    }
}

EmpDao 准备好之后最好测试一下,确认代码正确。养成随写随测的好习惯。

5.2.1.6.组件EmpService

在三层结构中,事务通常都是加到业务逻辑层,针对Service类使用事务

@Service
public class EmpService {
    private EmpDao empDao;

    @Autowired
    public void setEmpDao(EmpDao empDao) {
        this.empDao = empDao;
    }

    // 为了便于核对数据库操作结果,不要修改同一条记录
    // 修改员工姓名的一组参数 (empId4EditName,newName)
    // 修改员工工资的一组参数 (empId4EditSalary,newSalary)
    public void updateTwice(Integer empId4EditName, String newName, Integer empId4EditSalary, Double newSalary) {
        // 为了测试事务是否生效,执行两个数据库操作,看它们是否会在某一个失败时一起回滚
        empDao.updateEmpNameById(empId4EditName, newName);
        empDao.updateEmpSalaryById(empId4EditSalary, newSalary);
    }
}
5.2.1.7.测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:springContext.xml"})
public class JDBCTest {
    @Autowired
    private EmpService empService;

    @Test
    public void testJdbcTemplate4() {
        Integer empId4EditName = 700;
        String newName = "Trump";
        Integer empId4EditSalary = 800;
        Double newSalary = 8989.00;
        empService.updateTwice(empId4EditName, newName, empId4EditSalary, newSalary);
    }
}

正常执行两个更新操作

5.2.2.应用最基本的事务控制

5.2.2.1.加事务前状态
  1. 程序中增加错误

    public void updateTwice(Integer empId4EditName, String newName, Integer empId4EditSalary, Double newSalary) {
        // 为了测试事务是否生效,执行两个数据库操作,看它们是否会在某一个失败时一起回滚
        empDao.updateEmpNameById(empId4EditName, newName);
        int a = 10 / 0;
        empDao.updateEmpSalaryById(empId4EditSalary, newSalary);
    }
    
  2. 执行

    @Test
    public void testJdbcTemplate4() {
        Integer empId4EditName = 700;
        String newName = "Trump";
        Integer empId4EditSalary = 800;
        Double newSalary = 8989.00;
        empService.updateTwice(empId4EditName, newName, empId4EditSalary, newSalary);
    }
    
  3. 结果

    效果:修改姓名的操作生效了,修改工资的操作没有生效。

5.2.2.2.添加事务功能
  1. 配置事务管理器

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 事务管理器的bean只需要装配数据源,其他属性保持默认值即可 -->
        <property name="dataSource" ref="druidDataSource"/>
    </bean>
    
    
    <!-- 导入外部属性文件 -->
    <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>
    
  2. 开启基于注解的声明式事务功能

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 事务管理器的bean只需要装配数据源,其他属性保持默认值即可 -->
        <property name="dataSource" ref="druidDataSource"/>
    </bean>
    
    <!-- 开启基于注解的声明式事务功能 -->
    <!-- 使用transaction-manager属性指定当前使用是事务管理器的bean -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
    

    使用<tx:annotation-driven transaction-manager=“transactionManager”/>开启基于注解的声明式事务功能,使用transaction-manager属性指定当前使用是事务管理器的bean。transaction-manager属性的默认值是transactionManager,如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 ,即<tx:annotation-driven/>

    注意:导入名称空间时有好几个重复的,我们需要的是 tx 结尾的那个。

  3. 在需要事务的方法上使用@Transactional注解

    此时不设置的其他属性,只使用默认属性。

    @Transactional
    public void updateTwice(Integer empId4EditName, String newName, Integer empId4EditSalary, Double newSalary) {
        // 为了测试事务是否生效,执行两个数据库操作,看它们是否会在某一个失败时一起回滚
        empDao.updateEmpNameById(empId4EditName, newName);
        int a = 10 / 0;
        empDao.updateEmpSalaryById(empId4EditSalary, newSalary);
    }
    
  4. 测试结果

5.2.2.3.从日志角度查看事务
  1. 加入依赖

    <!-- 加入日志 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
    
  2. 加入logback的配置文件

    文件名: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>
    
  3. 日志中事务相关内容

    • 事务回滚时

    • 事务提交时

5.2.2.4.debug查看事务管理器中的关键方法

类:org.springframework.jdbc.datasource.DataSourceTransactionManager

  • 获取数据库链接

  • 开启事务的方法

  • 提交事务的方法

  • 回滚事务的方法

5.2.3.事务属性:只读

5.2.3.1.介绍

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

5.2.3.2.设置方式

readOnly = true把当前事务设置为只读

@Transactional(readOnly = true)
public String getEmpName(Integer empId) {
    return empDao.selectEmpNameById(empId);
}
5.2.3.3.针对增删改操作设置只读

会抛出下面异常:

5.2.3.4.@Transactional注解在类上
  1. 生效原则

    如果一个类中每一个方法上都使用了@Transactional注解,那么就可以将@Transactional注解提取到类上。反过来说:@Transactional注解在类级别标记,会影响到类中的每一个方法。同时,类级别标记的@Transactional注解中设置的事务属性也会延续影响到方法执行时的事务属性。除非在方法上又设置了@Transactional注解。

    对一个方法来说,离它最近的@Transactional注解中的事务属性设置生效。

  2. 用法举例

    在类级别@Transactional注解中设置只读,这样类中所有的查询方法都不需要设置@Transactional注解了。因为对查询操作来说,其他属性通常不需要设置,所以使用公共设置即可。

    然后在这个基础上,对增删改方法设置@Transactional注解 readOnly 属性为 false。

    @Service
    @Transactional(readOnly = true)
    public class EmpService {
        private EmpDao empDao;
    
        @Autowired
        private void setEmpDao(EmpDao empDao) {
            this.empDao = empDao;
        }
    
        @Transactional(readOnly = false)
        public void updateTwice(Integer empId4EditName, String newName) {
            empDao.updateEmpNameById(empId4EditName, newName);
        }
    
        public String getEmpName(Integer empId) {
            return empDao.selectEmpNameById(empId);
        }
    }
    

5.2.4.事务属性:超时

5.2.4.1.需求

事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。

此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。

概括来说就是一句话:超时回滚,释放资源

5.2.4.2.设置超时
  1. @Transactional注解中的设置

    @Transactional(readOnly = false, timeout = 3)
    public void updateEmp(Integer empId4EditName, String newName) {
        empDao.updateEmpNameById(empId4EditName, newName);
    }
    
  2. Dao方法中让线程睡眠

    public void updateEmpNameById(Integer empId, String empName) {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String sql = "update t_emp set emp_name=? where emp_id=?";
        jdbcTemplate.update(sql, empName, empId);
    }
    

    注意:sleep操作如果放在执行 SQL 语句后面那就不起作用。

  3. 执行效果

    @Test
    public void testJdbcTemplate5() {
        Integer empId4EditName = 700;
        String newName = "Trump";
        empService.updateEmp(empId4EditName, newName);
    }
    

    执行过程中日志和抛出异常的情况:

5.2.5.事务属性:回滚和不回滚的异常

5.2.5.1.默认情况

默认只针对运行时异常回滚,编译时异常不回滚。情景模拟代码如下:

public void updateEmpNameById(Integer empId, String empName) throws FileNotFoundException {
    String sql = "update t_emp set emp_name=? where emp_id=?";
    jdbcTemplate.update(sql, empName, empId);

	//抛出编译时异常测试不会回滚
    new FileInputStream("aaaa.aaa");

	//  抛出运行时异常测试会回滚
	//  System.out.println(10 / 0);
}

抛出运行时异常测试会回滚

5.2.5.2.设置回滚的异常

rollbackFor属性:需要设置一个Class类型的对象

rollbackForClassName属性:需要设置一个字符串类型的全类名

@Transactional(rollbackFor = Exception.class)

示例:

@Transactional(readOnly = false, timeout = 3, rollbackFor = Exception.class)
public void updateEmp(Integer empId4EditName, String newName) throws FileNotFoundException {
    // 为了测试事务是否生效,执行两个数据库操作,看它们是否会在某一个失败时一起回滚
    empDao.updateEmpNameById(empId4EditName, newName);
}
public void updateEmpNameById(Integer empId, String empName) throws FileNotFoundException {
    String sql = "update t_emp set emp_name=? where emp_id=?";
    jdbcTemplate.update(sql, empName, empId);
	System.out.println(10 / 0);
}
5.2.5.3.设置不回滚的异常

在默认设置和已有设置的基础上,再指定一个异常类型,碰到它不回滚。

@Transactional(noRollbackFor = FileNotFoundException.class)

示例:

// timeout属性:设置规定的超时时间,以秒为单位
// rollbackFor属性:指定要回滚的异常
// noRollbackFor属性:指定不回滚的异常
@Transactional(readOnly = false, timeout = 3, rollbackFor = Exception.class, noRollbackFor = FileNotFoundException.class)
public void updateEmp(Integer empId4EditName, String newName) throws FileNotFoundException {
    // 为了测试事务是否生效,执行两个数据库操作,看它们是否会在某一个失败时一起回滚
    empDao.updateEmpNameById(empId4EditName, newName);
}
5.2.5.4.回滚和不回滚异常同时设置
  1. 范围不同

    不管是哪个设置范围大,都是在大范围内在排除小范围的设定。例如:

    @Transactional(rollbackFor = Exception.class, noRollbackFor = FileNotFoundException.class)
    

    意思是除了FileNotFoundException 之外,其他所有 Exception 范围的异常都回滚;但是碰到 FileNotFoundException 不回滚。

  2. 范围一致

    回滚和不回滚的异常设置了相同范围

    @Transactional(rollbackFor = FileNotFoundException.class, noRollbackFor = FileNotFoundException.class)
    

    此时Spring 采纳了rollbackFor属性的设定:遇到 FileNotFoundException 异常会回滚。

5.2.6.隔离级别概念回顾

隔离级别是为了解决并发问题

5.2.6.1.并发问题
并发问题问题描述
脏读当前事务读取了其他事务尚未提交的修改,如果那个事务回滚,那么当前事务读取到的修改就是错误的数据
不可重复读当前事务读取同一个数据,第一次和第二次不一致
幻读当前事务在执行过程中,数据库表增减或减少了一些记录,感觉像是出现了幻觉
5.2.6.2.隔离级别
隔离级别描述能解决的并发问题
读未提交允许当前事务读取其他事务尚未提交的修改啥问题也解决不了
读已提交允许当前事务读取其他事务已经提交的修改脏读
可重复读当前事务执行时锁定当前记录,不允许其他事务操作脏读、不可重复读
串行化当前事务执行时锁定当前表,不允许其他事务操作脏读、不可重复读、幻读

5.2.7.事务属性:事务隔离级别

5.2.7.1.视角提升

前面的只读、超时、回滚不回滚都是一个事务内部属性,没有讨论事务与事务之前的关系。隔离级别、传播行为是对应事务之间的关系。

5.2.7.2.测试的准备工作
  1. 测试思路

  2. EmpService中参与测试的方法

    @Transactional(readOnly = true)
    public String getEmpName(Integer empId) {
        return empDao.selectEmpNameById(empId);
    }
    
    @Transactional(readOnly = false)
    public void updateEmpName(Integer empId, String empName) {
        empDao.updateEmpNameById(empId, empName);
    }
    
  3. Junit中执行测试的方法

    @Test
    public void testTxReadOnly() {
        String empName = empService.getEmpName(700);
        System.out.println("empName = " + empName);
    }
    
    @Test
    public void testIsolation() {
        Integer empId = 700;
        String empName = "aaaaaaaa";
        empService.updateEmpName(empId, empName);
    }
    
5.2.7.3.测试准备

为了让事务B(执行修改操作的事务)能够回滚,在EmpDao中的对应方法中人为抛出异常。

public void updateEmpNameById(Integer empId, String empName) {
    String sql = "update t_emp set emp_name=? where emp_id=?";
    jdbcTemplate.update(sql, empName, empId);
    System.out.println(10 / 0);
}
5.2.7.4.测试隔离级别为读未提交

在 @Transactional 注解中使用 isolation 属性设置事务的隔离级别。 取值使用 org.springframework.transaction.annotation.Isolation 枚举类提供的数值。

@Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED)
public String getEmpName(Integer empId) {
    return empDao.selectEmpNameById(empId);
}

@Transactional(readOnly = false, isolation = Isolation.READ_UNCOMMITTED)
public void updateEmpName(Integer empId, String empName) {
    empDao.updateEmpNameById(empId, empName);
}
  1. 在org.springframework.jdbc.datasource.DataSourceTransactionManager#doRollback设置debug

  2. 执行update操作

    执行update操作,在updateEmpNameById中增加运行异常,数据不会提交,会回滚。

    此时查询数据库没有显示未提交的数据

    此时查询数据库显示未修改数据

  3. 在updateEmpNameById方法未提交前执行testTxReadOnly查询操作,查询数据。显示的updateEmpNameById更新的数据,但是此时更新操作未提交,出现脏读

  4. 放开updateEmpNameById方法执行回滚操作

  5. 再次执行testTxReadOnly查询操作

5.2.7.5.测试隔离级别为读未提交
@Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED)
public String getEmpName(Integer empId) {
    return empDao.selectEmpNameById(empId);
}

@Transactional(readOnly = false, isolation = Isolation.READ_COMMITTED)
public void updateEmpName(Integer empId, String empName) {
    empDao.updateEmpNameById(empId, empName);
}

测试步骤和之前一样,在update回滚前执行查询操作,看是否出现脏读。

执行update语句回滚前查询,此时未出现脏读。

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

5.2.8.事务属性:事务传播行为

5.2.8.1.事务传播行为要研究的问题

java程序都在线程中执行的,没有创建多的线程就是在主线程执行。

较早执行的方法已经开启事务了,对于后面需要事务的方法怎么传播过去。这就是事务传播行为要研究的问题。

即在一个线程上多个方法都需要事务,那他们是公用一个事务还是自己开启事务。

5.2.8.2.propagation属性
  1. 默认值

    @Transactional注解通过propagation属性设置事务的传播行为。它的默认值是:

    Propagation propagation() default Propagation.REQUIRED;
    
  2. 可选值说明

    propagation属性的可选值由org.springframework.transaction.annotation.Propagation枚举类提供:

    只需要了解默认值REQUIRED和REQUIRES_NEW即可。

    名称含义
    REQUIRED默认值(重点)当前方法必须工作在事务中
    如果当前线程上有已经开启的事务可用,那么就在这个事务中运行;
    如果当前线程上没有已经开启的事务,那么就自己开启新事务,在新事务中运行;
    所以当前方法有可能和其他方法共用事务,在共用事务的情况下,当前方法会因为其他方法回滚而受连累;
    REQUIRES_NEW 建议使用(重点)当前方法必须工作在事务中
    不管当前线程上是否有已经开启的事务,都要开启新事务
    在新事务中运行不会和其他方法共用事务,避免被其他方法回滚连累。
    SUPPORTS支持当前事务,若当前没有事务,则以非事务方式执行
    MANDATORY使用当前事务,若当前没有事务,则抛出异常
    NOT_SUPPORTED以非事务方式执行操作,若当前存在事务,则把当前事务挂起
    NEVER以非事务方式执行,如果当前存在事务,则抛出异常
    NESTED如当前存在事务,则嵌套事务内执行,若没有事务则执行PROPAGATION_REQUIRED类似操作
5.2.8.3.测试准备
  1. 在EmpService中声明两个内层方法

    @Service
    @Transactional(readOnly = true)
    public class EmpService {
        private EmpDao empDao;
    
        @Transactional(readOnly = false)
        public void updateEmpNameInner(Integer empId, String empName) {
            empDao.updateEmpNameById(empId, empName);
        }
    
        @Transactional(readOnly = false)
        public void updateEmpSalaryInner(Integer empId, Double empSalary) {
            empDao.updateEmpSalaryById(empId, empSalary);
        }
    }
    
  2. 创建TopService

    @Service
    public class TopService {
        // 这里我们只是为了测试事务传播行为,临时在Service中装配另一个Service
        // 实际开发时非常不建议这么做,因为这样会严重破坏项目的结构
        @Autowired
        private EmpService empService;
    
        @Transactional
        public void topTxMethod() {
            // 在外层方法中调用两个内层方法
            empService.updateEmpNameInner(700, "Trump");
            empService.updateEmpSalaryInner(800, 666.66);
        }
    }
    
  3. junit测试方法

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(value = {"classpath:springContext.xml"})
    public class JDBCTest {
        @Autowired
        private TopService topService;
    
        @Test
        public void testPropagation() {
            // 调用外层方法
            topService.topTxMethod();
        }
    }
    
5.2.8.4.测试REQUIRED模式

执行流程

效果:内层方法A、内层方法B所做的修改都没有生效,总事务回滚了。

总体都会被回滚,数据库不会被修改任何信息。

5.2.8.5.测试REQUIRES_NEW模式

执行流程

5.2.8.6.实际开发情景
  1. Service方法应用了通知

  2. 过滤器或拦截器等类似组件

  3. 总结

    我们在事务传播行为这里,使用 REQUIRES_NEW 属性,也可以说是让不同事务方法从事务的使用上解耦合,不要互相影响。

5.3.基于XML的声明式事务

5.3.1.加入依赖

相比于基于注解的声明式事务,基于 XML 的声明式事务需要一个额外的依赖:

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

5.3.2.迁移代码

将上一个基于注解的 module 中的代码转移到新module。去掉 @Transactional 注解。

5.3.3.修改 Spring 配置文件

去掉 tx:annotation-driven 标签,然后加入下面的配置:

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

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

    <!-- 导入外部属性文件 -->
    <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>
    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 事务管理器的bean只需要装配数据源,其他属性保持默认值即可 -->
        <property name="dataSource" ref="druidDataSource"/>
    </bean>
    <!-- 配置 JdbcTemplate -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <!-- 装配数据源 -->
        <property name="dataSource" ref="druidDataSource"/>
    </bean>

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

6.整合junit5

6.1.导入依赖

在原有环境基础上增加如下依赖:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.7.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.1</version>
</dependency>

6.2.创建测试类

@ExtendWith(SpringExtension.class) 表示使用 Spring 提供的扩展功能。

@ContextConfiguration(value = {“classpath:spring-context.xml”}) 还是用来指定 Spring 配置文件位置,和整合 junit4 一样。

@ExtendWith(SpringExtension.class)
@ContextConfiguration(value = {"classpath:spring-context.xml"})
public class Junit5IntegrationTest {
    @Autowired
    private EmpDao empDao;
    
    @Test
    public void testJunit5() {
        System.out.println("empDao = " + empDao);
    }
}

6.3.使用复合注解

@SpringJUnitConfig 注解综合了前面两个注解的功能,此时指定 Spring 配置文件位置即可。但是注意此时需要使用 locations 属性,不是 value 属性了。

@SpringJUnitConfig(locations = {"classpath:spring-context.xml"})
public class Junit5IntegrationTest {
    @Autowired
    private EmpDao empDao;
    
    @Test
    public void testJunit5() {
        System.out.println("empDao = " + empDao);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值