侧边栏壁纸
博主头像
威风博主等级

九万里风鹏正举,风休住,蓬舟吹取三山去。

  • 累计撰写 38 篇文章
  • 累计创建 23 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Spring5

威风
2021-03-20 / 0 评论 / 0 点赞 / 105 阅读 / 77,772 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2021-11-06,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

1、参考资料

  1. 尚硅谷 - Spring5 框架最新版教程(idea 版)

  2. 雷丰阳 spring、springmvc、mybatis、spring 一站式学习

2、Spring 概述

2.1、Spring 框架概述

Spring 是轻量级的开源的 JavaEE 框架

Spring 为简化企业级开发而生,使用 Spring,Javabean 就可以实现很多以前要靠 EJB 才能实现的功能

Spring 有两个核心部分:IOC 和 AOP

  1. IOC(Inversion of Control,即控制反转)是面向对象编程中的一种设计原则,可以用来降低计算机代码之间的耦合度,其中最常见的方式叫做依赖注入(Dependency Injection,简称 DI)。Spring 就是采用依赖注入的方式,为我们管理容器中的 bean 实例对象

  2. AOP(Aspect Oriented Programming,即面向切面)可以在不修改源代码的前提下,通过预编译方式和运行期间动态代理方式实现对原有代码的增强(添加新功能)

Spring 的优良特性

  1. 非侵入式:基于 Spring 开发的应用中的对象可以不依赖于 Spring 的 API

  2. 依赖注入:DI——Dependency Injection,反转控制 (IOC) 最经典的实现

  3. 面向切面编程:Aspect Oriented Programming——AOP

  4. 容器:Spring 是一个容器,因为它包含并且管理应用对象的生命周期

  5. 组件化:Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML 和 Java 注解组合这些对象。

  6. 一站式:在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(实际上 Spring 自身也提供了表述层的 SpringMVC 和持久层的 Spring JDBC)。

Spring 目前的版本

目前已经更新到 Spring 5.3.3 版本,不过我还是与老师使用同一个版本吧,老师讲课使用的是 Spring 5.2.6 版本

image

Spring jar 包下载地址

老师上课手动导入 jar 包的依赖,我们还是使用 maven 引入相关依赖吧~

image

Spring 模块

image

2.2、Spring 入门案例

0、入门案例的目标

目标:使用 Spring 创建 bean 实例对象,并为其属性赋值

1、创建 maven 工程

因为后续测试还会创建其他 maven 工程,因此我们先创建一个 maven 父工程(spring-learn),再创建本章对应的 maven 子工程(spring-getting-start)

image

2、引入相关依赖

在 spring-getting-start 工程中引入 spring-beansspring-contextspring-corespring-expression 的依赖,这四个依赖正是 Spring Core Container 所需的依赖,此外 Springn 还需依赖 commons-logging 实现日志功能

image

如下是 pom.xml 中引入的依赖配置,为了测试方便,我们引入了 spring-testjupiter 相关依赖

<dependencies>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>

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

    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>

    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-expression</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>

    
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.2</version>
    </dependency>

    
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>RELEASE</version>
        <scope>test</scope>
    </dependency>

    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.2.6.RELEASE</version>
        <scope>test</scope>
    </dependency>
</dependencies>

3、创建实体类

创建 Student 实体类

public class Student {

    private Integer stuId;
    private String stuName;

    public Student() {
        
    }

    public Student(Integer stuId, String stuName) {
        this.stuId = stuId;
        this.stuName = stuName;
    }

    public Integer getStuId() {
        return stuId;
    }

    public String getStuName() {
        return stuName;
    }

    public void setStuId(Integer stuId) {
        this.stuId = stuId;
    }

    public void setStuName(String stuName) {
        this.stuName = stuName;
    }

    @Override
    public String toString() {
        return "Student{" +
                "stuId=" + stuId +
                ", stuName='" + stuName + '\'' +
                '}';
    }

}

4、编写 Spring 配置文件

在 resources 包下点击鼠标右键,选择【New】–>【XML Configuration File】–>【Spring Config】,注:resource 包下的配置文件在执行时会被拷贝至类路径的根目录

image

添加如下配置:创建 Student 对象的实例,并注入属性的值

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

    
    
    
    <bean id="student" class="com.oneby.entity.Student">
        
        <property name="stuId" value="007"/>
        <property name="stuName" value="Oneby"/>
    </bean>

</beans>

5、编写测试代码

测试代码:首先创建 ClassPathXmlApplicationContext 对象,XML 配置文件的路径为类路径下的 getting-start.xml;然后获取容器中的 Student 对象,并打印此 Student 对象

public class SpringTest {

    @Test
    public void gettingStart() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("getting-start.xml");

        
        Student student = (Student) iocContainer.getbean("student");

        
        System.out.println(student);
    }

}

测试结果:从 Spring 容器中获取到的 Student 对象,其属性值已经被注入

image

关于 jupiter 更加拉风的写法

使用 @SpringJUnitConfig(locations = "classpath:getting-start.xml") 注解指明 Spring 单元测试的配置文件路径,再使用 @Autowired 注解自动装配容器中的 Student 对象

@SpringJUnitConfig(locations = "classpath:getting-start.xml")
public class SpringTest {

    @Autowired
    private Student student;

    @Test
    public void gettingStart() {
        System.out.println(student);
    }

}

测试结果:为啥这个就输出了一大堆日志信息

image

测试结果:Spring 在创建 IOC 容器时,就已经完成了 bean 的创建和属性的赋值

3、IOC 容器和 bean 管理

3.1、IOC 和 DI 的关系

IOC(Inversion of Control):反转控制

早在 2004 年,Martin Fowler 就提出了 “哪些方面的控制被反转了?” 这个问题。他总结出是依赖对象的获得被反转了,因为大多数应用程序都是由两个或是更多的类通过彼此的合作来实现企业逻辑,这使得每个对象都需要获取与其合作的对象(也就是它所依赖的对象)的引用。如果这个获取过程要靠自身实现,那么这将导致代码高度耦合并且难以维护和调试

在应用程序中的组件需要获取资源时,传统的方式是组件主动的从容器中获取所需要的资源,在这样的模式下开发人员往往需要知道在具体容器中特定资源的获取方式。比如 ClassA 中需要用到 ClassB 的对象,一般情况下,需要在 ClassA 的代码中显式的 new 一个 ClassB 的对象

反转控制的思想完全颠覆了应用程序组件获取资源的传统方式:反转了资源的获取方向——改由容器主动的将资源推送给需要的组件,开发人员不需要知道容器是如何创建资源对象的,只需要提供接收资源的方式即可。采用依赖注入技术之后,ClassA 的代码只需要定义一个私有的 ClassB 对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将 ClassB 对象在外部 new 出来并注入到 A 类里的引用中。而具体获取的方法、对象被获取时的状态由配置文件(如 XML)来指定

DI(Dependency Injection):依赖注入

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

IOC 容器在 Spring 中的实现

在通过 IOC 容器读取 bean 的实例之前,需要先将 IOC 容器本身实例化,Spring 提供了 IOC 容器的两种实现方式:

  1. beanFactory:IOC 容器的基本实现,是 Spring 内部的基础设施,是面向 Spring 本身的,不是提供给开发人员使用的。

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

image

ApplicationContext的主要实现类

  1. ClassPathXmlApplicationContext:对应类路径下的 XML 格式的配置文件

  2. FileSystemXmlApplicationContext:对应文件系统中的 XML 格式的配置文件

在初始化时就创建单例的 bean,也可以通过配置的方式指定创建的 bean 是多实例的

ConfigurableApplicationContext 接口

  1. ApplicationContext的子接口,包含一些扩展方法

  2. refresh()close()ApplicationContext具有启动、关闭和刷新上下文的能力

WebApplicationContext 接口

WebApplicationContext, 是继承于ApplicationContext的一个接口,扩展了ApplicationContext,是专门为 Web 应用准备的,它允许从相对于 Web 根目录的路径中装载配置文件完成初始化

举例说明 IOC 为何可以降低代码的耦合度

1、原始方式:自己 new 对象

假设现在需要创建 Student 对象实例,原始方式则是直接 new Student() 对象,再通过 setter 方法注入器属性值

Student stu = new Student();
stu.setStuId(7);
stu.setStuName("Oneby");

结论:我们纯手动自己 new 对象,代码耦合度极高

2、进阶方式:通过工厂创建对象

可以先通过 XML 配置文件配置 bean 的属性

<bean id="student" class="com.oneby.entity.Student">
    <property name="stuId" value="007"/>
    <property name="stuName" value="Oneby"/>
</bean>

再通过工厂模式 + 反射的方法创建该对象的实例,并注入属性值

public class StudentFactory {
    public static Student getStudent(){
        String className = ...; 
        String[] fieldNames = ..; 
        String[] fieldValues = ...; 
        Class clazz = Class.forName(className); 
        for (int i = 0; i < fieldNames.length; i++) {
            
        }
        return clazz; 
    }
}

结论:这种方式可以降低代码的耦合度,我们使用 Student 对象再不需要自己去 new,而是通过工厂获得,但是这种方式还是脱离不了我们自己去获取和管理 bean

3、最终方式:通过 Spring IOC 管理 bean

首先创建 Spring 配置文件

<bean id="student" class="com.oneby.entity.Student">
    <property name="stuId" value="007"/>
    <property name="stuName" value="Oneby"/>
</bean>

在通过 iocContainer.getbean("beanId") 方法或者 @Autowire 方式获取 bean 岂不美滋滋,这样我们可以将 bean 的创建与它们之间的依赖关系完全交给 Spring IOC 容器去管理,代码耦合程度极大降低

3.2、bean 对象的获取

获取 bean 对象的几种方式

在容器的顶级接口 beanFactory接口中,定义了如下几个方法,用于获取 bean 实例

  1. Object getbean(String name) throws beansException;:通过 bean name 获取 bean 实例

  2. <T> T getBean(Class<T> requiredType) throws BeansException;:通过 bean class 获取 bean 实例

  3. <T> T getBean(String name, Class<T> requiredType) throws BeansException;:通过 bean name 和 bean class 获取 bean 实例

3.3、bean 属性的赋值

3.3.1、属性注入的方式

1、通过 bean 的 setter 方法注入属性

通过 <property> 标签指定属性名,Spring 会帮我们找到该属性对应的 setter 方法,注入其属性值

<bean id="student" class="com.oneby.entity.Student">
    <property name="stuId" value="007"/>
    <property name="stuName" value="Oneby"/>
</bean>

测试结果

image

2、通过构造器注入属性值

通过 constructor-arg 标签为对象的属性赋值,通过 name 指定属性名,value 指定属性值

<bean id="student" class="com.oneby.entity.Student">
    <constructor-arg name="stuId" value="1" />
    <constructor-arg name="stuName" value="Oneby" />
</bean>

测试结果

image

3、通过级联属性赋值

准备工作

为了演示级联属性的赋值,我们添加 Computer

public class Computer {

    String computerId;
    String computerName;

    public Computer() {
    }

    public Computer(String computerId, String computerName) {
        this.computerId = computerId;
        this.computerName = computerName;
    }

    public String getComputerId() {
        return computerId;
    }

    public String getComputerName() {
        return computerName;
    }

    public void setComputerId(String computerId) {
        this.computerId = computerId;
    }

    public void setComputerName(String computerName) {
        this.computerName = computerName;
    }

    @Override
    public String toString() {
        return "Computer{" +
                "computerId='" + computerId + '\'' +
                ", computerName='" + computerName + '\'' +
                '}';
    }
}

Student 类中添加 Computer 类型的字段

public class Student {

    private Integer stuId;
    private String stuName;
    private Computer computer;

    public Student() {

    }

    public Student(Integer stuId, String stuName, Computer computer) {
        this.stuId = stuId;
        this.stuName = stuName;
        this.computer = computer;
    }

    public Integer getStuId() {
        return stuId;
    }

    public String getStuName() {
        return stuName;
    }

    public Computer getComputer() {
        return computer;
    }

    public void setStuId(Integer stuId) {
        this.stuId = stuId;
    }

    public void setStuName(String stuName) {
        this.stuName = stuName;
    }

    public void setComputer(Computer computer) {
        this.computer = computer;
    }

    @Override
    public String toString() {
        return "Student{" +
                "stuId=" + stuId +
                ", stuName='" + stuName + '\'' +
                ", computer=" + computer +
                '}';
    }

}

演示级联属性的赋值

student 对象中有一个名为 computer 的字段,该字段指向了一个 computer 对象,级联属性赋值的含义为:在 student bean 中的 <property> 标签中可以通过 computer.computerIdcomputer.computerName 的方式为 computer 对象中的 computerIdcomputerName 字段赋值

<bean id="student" class="com.oneby.entity.Student">
    <property name="computer" ref="computer"/>
    
    <property name="computer.computerId" value="233"/>
    <property name="computer.computerName" value="HP"/>
</bean>

<bean id="computer" class="com.oneby.entity.Computer"/>

测试结果

image

4、通过 p 名称空间注入属性值

为了简化 XML 文件的配置,越来越多的 XML 文件采用属性而非子元素配置信息。Spring 从 2.5 版本开始引入了一个新的 p 命名空间,可以通过<bean>元素属性的方式配置 Bean 的属性。使用 p 命名空间后,基于 XML 的配置方式将进一步简化。

准备工作:在配置文件中引入 p 名称空间

对照着 xmlns="http://www.springframework.org/schema/beans" 这行改吧改吧

image

通过 p 名称空间注入属性值

其实 p 名称空间赋值就是 <property> 标签赋值的简化版

<bean id="student" class="com.oneby.entity.Student" p:stuId="1" p:stuName="Oneby"/>

测试结果

image

3.3.2、属性赋值的类型

1、字面量

  1. 可以使用字符串表示的值,可以通过value属性或value子节点的方式指定

  2. 基本数据类型及其封装类、String等类型都可以采取字面值注入的方式

  3. 若字面值中包含特殊字符,可以使用<![CDATA[]]>把字面值包裹起来

演示字面量的使用

<bean id="student" class="com.oneby.entity.Student">
    <property name="stuId" value="233"/>
    <property name="stuName" value="Oneby"/>
</bean>

2、null 值

通过 <null/> 标签将引用类型字段的值设置为 null

<bean id="student" class="com.oneby.entity.Student">
    <property name="stuId" value="233"/>
    
    <property name="stuName">
        <null/>
    </property>
    
    <property name="computer">
        <null/>
    </property>
</bean>

测试结果

image

3、引用外部 bean

通过 <property> 标签中的 ref 属性引用外部 bean

<bean id="student" class="com.oneby.entity.Student">
    <property name="stuId" value="233"/>
    <property name="stuName" value="Oneby"/>
    
    <property name="computer" ref="computer"/>
</bean>

<bean id="computer" class="com.oneby.entity.Computer">
    <property name="computerId" value="255"/>
    <property name="computerName" value="HP"/>
</bean>

测试结果

image

4、引用内部 bean

当 bean 实例仅仅给一个特定的属性使用时,可以将其声明为内部 bean。内部 bean 声明直接包含在<property><constructor-arg>元素里,不需要设置任何idname属性,内部 bean 不能使用在任何其他地方

<property> 标签中不使用 ref 属性引用外部 bean,而是直接定义一个 内部 bean

<bean id="student" class="com.oneby.entity.Student">
    <property name="stuId" value="233"/>
    <property name="stuName" value="Oneby"/>
    <property name="computer">
        
        <bean class="com.oneby.entity.Computer">
            <property name="computerId" value="255"/>
            <property name="computerName" value="HP"/>
        </bean>
    </property>
</bean>

测试结果

image

3.3.3、对集合属性赋值

在 Spring 中可以通过一组内置的 XML 标签来配置集合属性,比如:<array><list><set><map><props>,并且可以用过引入 util 名称空间来提取集合类型的 bean

0、准备工作

新建 CollectionExample 类,演示对集合属性的操作

public class CollectionExample {

    private String[] array;
    private List<String> list;
    private Set<String> set;
    private Map<String,String> map;
    private Properties properties;

    public String[] getArray() {
        return array;
    }

    public void setArray(String[] array) {
        this.array = array;
    }

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

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

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

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

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

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

    public Properties getProperties() {
        return properties;
    }

    public void setProperties(Properties properties) {
        this.properties = properties;
    }

    @Override
    public String toString() {
        return "CollectionExample{" +
                "array=" + Arrays.toString(array) +
                ", list=" + list +
                ", set=" + set +
                ", map=" + map +
                ", properties=" + properties +
                '}';
    }
}

单元测试代码

public class SpringTest {

    @Test
    public void test() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("collection-property-injection.xml");

        
        CollectionExample collectionExample = (CollectionExample) iocContainer.getBean("collectionExample");

        
        System.out.println(collectionExample);
    }

}

1、数组

通过 <array> 标签定义数组集合,并且可以通过<value>指定简单的常量值,通过<ref>指定对其他 bean 的引用。通过<bean>指定内置 bean 定义。通过<null/>指定空元素,甚至可以内嵌其他集合。

<bean id="collectionExample" class="com.oneby.entity.CollectionExample">
    <property name="array">
        <array>
            <value>Oneby</value>
            <value>Heygo</value>
        </array>
    </property>
</bean>

测试结果

image

2、List

通过 <list> 标签定义数组集合,并且可以通过<value>指定简单的常量值,通过<ref>指定对其他 bean 的引用。通过<bean>指定内置 bean 定义。通过<null/>指定空元素,甚至可以内嵌其他集合。

<bean id="collectionExample" class="com.oneby.entity.CollectionExample">
    <property name="list">
        <list>
            <value>Oneby</value>
            <value>Heygo</value>
        </list>
    </property>
</bean>

测试结果

image

3、Set

通过 <set> 标签定义数组集合,并且可以通过<value>指定简单的常量值,通过<ref>指定对其他 bean 的引用。通过<bean>指定内置 bean 定义。通过<null/>指定空元素,甚至可以内嵌其他集合。

<bean id="collectionExample" class="com.oneby.entity.CollectionExample">
    <property name="set">
        <set>
            <value>Oneby</value>
            <value>Heygo</value>
        </set>
    </property>
</bean>

测试结果

image

4、Map

Java.util.Map通过<map>标签定义,<map>标签里可以使用多个<entry>作为子标签,每个<entry>中包含一个键和一个值。因为键和值的类型没有限制,所以可以自由地为它们指定<value><ref><bean><null/>元素。因此对于常量型的key-value键值对可以使用keyvalue来定义;bean引用通过key-refvalue-ref属性定义。

<bean id="collectionExample" class="com.oneby.entity.CollectionExample">
    <property name="map">
        <map>
            <entry key="name" value="Oneby"></entry>
            <entry key="hobby" value="code"></entry>
        </map>
    </property>
</bean>

测试代码

image

5、Properties

使用<props>定义java.util.Properties,该标签使用多个<prop>作为子标签,每个<prop>标签中定义keyvalue

<bean id="collectionExample" class="com.oneby.entity.CollectionExample">
    <property name="properties">
        <props>
            <prop key="name">Oneby</prop>
            <prop key="hobby">code</prop>
        </props>
    </property>
</bean>

测试代码

image

6、集合类型的 bean

如果只能将集合对象配置在某个 bean 内部,则这个集合的配置将不能重用。我们需要将集合 bean 的配置拿到外面,供其他 bean 引用。

引入名称空间:配置集合类型的 bean 需要引入 util 名称空间

beans 名称空间对应的这两项 xmlns:util="http://www.springframework.org/schema/utilhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd,将 beans 全部替换为 util 就行啦~

image

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

使用 <util> 标签完成对集合类型 bean 的抽取,并为其设置 id 属性,方便其他地方进行引用

<util:list id="list">
    <value>Oneby</value>
    <value>Heygo</value>
</util:list>
<bean id="collectionExample" class="com.oneby.entity.CollectionExample">
    <property name="list" ref="list"/>
</bean>

测试结果

image

3.4、通过工厂创建 bean

1、静态工厂

调用静态工厂方法创建 bean 是将对象创建的过程封装到静态方法中。当客户端需要对象时,只需要简单地调用静态方法,而不用关心创建对象的细节。

声明通过静态方法创建的 bean 需要在<bean>class属性里指定静态工厂类的全类名,同时在factory-method属性里指定工厂方法的名称。最后使用<constrctor-arg>元素为该方法传递方法参数。

2、实例工厂

实例工厂方法:将对象的创建过程封装到另外一个对象实例的方法里。当客户端需要请求对象时,只需要简单的调用该实例方法而不需要关心对象的创建细节。

实现方式

  1. 配置工厂类实例的 bean

  2. factory-method属性里指定该工厂方法的名称

  3. 使用construtor-arg元素为工厂方法传递方法参数

3、FactoryBean 接口

以上两种方式通常用的不多,我们一般通过实现 FactoryBean 接口,并重写其中的方法来获得工厂类

  1. Spring 中有两种类型的 bean,一种是普通 bean,另一种是工厂 bean,即FactoryBean

  2. 工厂 bean 跟普通 bean 不同,其返回的对象不是指定类的一个实例,其返回的是该工厂 bean 的getObject()方法所返回的对象

  3. 工厂 bean 必须实现org.springframework.beans.factory.FactoryBean接口

FactoryBean 接口

FactoryBean 接口中有如下三个方法,getObject() 方法负责将创建好的 bean 实例返回给 IOC 容器;getObjectType() 方法负责返回工厂生产的 bean 类型;isSingleton() 方法用于指示该 bean 实例是否为单例,默认是单例 bean

image

public interface FactoryBean<T> {
    
    
    @Nullable
    T getObject() throws Exception;
    
    
    @Nullable
    Class<?> getObjectType();
    
    
    default boolean isSingleton() {
        return true;
    }

演示 FactoryBean 接口的使用

创建 StudentFactory 类,该类实现了 FactoryBean 接口,并重写了其中的 getObject()getObjectType() 方法

public class StudentFactory implements FactoryBean<Student> {
    @Override
    public Student getObject() throws Exception {
        return new Student(233,"Oneby");
    }

    @Override
    public Class<?> getObjectType() {
        return Student.class;
    }
}

在 Spring 配置文件中使用 StudentFactory 工厂创建 Student 对象

<bean id="student" class="com.oneby.entity.StudentFactory"/>

测试结果

image

3.5、bean 的高级配置

3.5.1、bean 的作用域

bean 的作用域

在 Spring 中,可以在<bean>元素的scope属性里设置 bean 的作用域,以决定这个 bean 是单实例的还是多实例的。

默认情况下,Spring 只为每个在 IOC 容器里声明的 bean 创建唯一一个实例(单例对象),整个 IOC 容器范围内都能共享该实例:所有后续的getBean()调用和 bean 引用都将返回这个唯一的 bean 实例。该作用域被称为singleton,它是所有 bean 的默认作用域

两种 bean 的作用域

singleton:在 Spring IOC 容器中仅存在一个 bean 实例,bean 以单实例的方式存在

prototype:每次调用getBean()时都会返回一个新的实例

:当 bean 的作用域为singleton时,Spring 会在 IOC 容器对象创建时就创建 bean 的对象实例。而当 bean 的作用域为prototype时,IOC 容器在获取 bean 的实例时创建 bean 的实例对象

单例模式:scope="singleton"

其实 scope 属性默认就是 singleton,不写也没事儿

<bean id="student" class="com.oneby.entity.Student" scope="singleton">
    <property name="stuId" value="233" />
    <property name="stuName" value="Oneby" />
</bean>

测试代码

public class SpringTest {

    @Test
    public void test() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("spring-advance-config.xml");

        
        Student student1 = (Student) iocContainer.getBean("student");
        Student student2 = (Student) iocContainer.getBean("student");

        
        System.out.println(student1);
        System.out.println(student2);
        System.out.println(student1 == student2);
    }

}

程序运行结果:调用两次 getBean() 方法得到的 Student 对象是同一个实例

image

原型模式:scope="prototype"

Student 对象的 scope 属性配置为 prototype,表示每次获取时才创建对象

<bean id="student" class="com.oneby.entity.Student" scope="prototype">
    <property name="stuId" value="233" />
    <property name="stuName" value="Oneby" />
</bean>

程序运行结果:调用两次 getBean() 方法将得到不同的 Student 对象

image

3.5.2、bean 的生命周期

Spring IOC 管理下的 bean 生命周期

Spring IOC 容器可以管理 bean 的生命周期,Spring 允许在 bean 生命周期内特定的时间点执行指定的任务,Spring IOC 容器对 bean 的生命周期进行管理的过程:

  1. 通过构造器或工厂方法创建 bean 实例

  2. 为 bean 的属性设置值和对其他 bean 的引用

  3. 调用 bean 的初始化方法

  4. bean 可以使用了

  5. 当容器关闭时,调用 bean 的销毁方法

注:在配置 bean 时,通过init-methoddestroy-method属性为 bean 指定初始化和销毁方法

代码演示

创建 Order 类,用于演示 bean 的生命周期

public class Order {

    private String name;

    public Order() {
        System.out.println("第一步:执行无参数构造创建 bean 实例");
    }

    public void setName(String name) {
        this.name = name;
        System.out.println("第二步:调用 setter 方法为属性赋值");
    }

    
    public void initMethod(){
        System.out.println("第三步:执行 init-method 初始化方法");
    }

    
    public void destroyMethod(){
        System.out.println("第五步:执行 destroy-method 初销毁方法");
    }

}

<bean> 标签中指定 order 对象的 init-method 方法(初始化方法)和 destroy-method 方法(销毁方法)

<bean id="order" class="com.oneby.entity.Order"
      init-method="initMethod" destroy-method="destroyMethod">
    <property name="name" value="iPad" />
</bean>

测试代码:记得要关闭 IOC 容器才会执行 destroy-method 方法,并且接口类型需要上升到 ConfigurableApplicationContext 才会提供 close() 方法

public class SpringTest {

    @Test
    public void test() {
        
        ConfigurableApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("spring-advance-config.xml");

        
        Order order = (Order) iocContainer.getBean("order");

        
        System.out.println("第四步:使用创建好的 order 对象" + order);

        
        iocContainer.close();
    }

}

程序运行结果

image

添加 BeanPostProcessor 后的 bean 生命周期

bean 的后置处理器

  1. bean 后置处理器允许在调用初始化方法前后对 bean 进行额外的处理

  2. bean 后置处理器对 IOC 容器里的所有 bean 实例逐一处理,而非单一实例。其典型应用是:检查 bean 属性的正确性或根据特定的标准更改 bean 的属性。

  3. bean 后置处理器时需要实现接口:org.springframework.beans.factory.config.BeanPostProcessor。在初始化方法被调用前后,Spring 将把每个 bean 实例分别传递给上述接口的以下两个方法:

  4. postProcessBeforeInitialization(Object, String)

  5. postProcessAfterInitialization(Object, String)

添加 bean 后置处理器后 bean 的生命周期

  1. 通过构造器或工厂方法创建 bean 实例

  2. 为 bean 的属性设置值和对其他 bean 的引用

  3. 将 bean 实例传递给 bean 后置处理器的 postProcessBeforeInitialization() 方法

  4. 调用 bean 的初始化方法

  5. 将 bean 实例传递给 bean 后置处理器的 postProcessAfterInitialization() 方法

  6. bean 可以使用了

  7. 当容器关闭时调用 bean 的销毁方法

代码演示

创建 MyBeanPost 类,继承自 MyBeanPost 类,并重写其中的 postProcessBeforeInitializationpostProcessAfterInitialization 方法

public class MyBeanPost implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第三步:执行 postProcessBeforeInitialization 方法");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("第五步:执行 postProcessAfterInitialization 方法");
        return bean;
    }
}

配置文件:在配置文件中实例化我们自定义的 MyBeanPost 后置处理器

<bean id="order" class="com.oneby.entity.Order"
      init-method="initMethod" destroy-method="destroyMethod">
    <property name="name" value="iPad" />
</bean>


<bean id="myBeanPost" class="com.oneby.config.MyBeanPost"/>

测试代码:现在使用 order 对象变成了第六步

public class SpringTest {

    @Test
    public void test() {
        
        ConfigurableApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("spring-advance-config.xml");

        
        Order order = (Order) iocContainer.getBean("order");

        
        System.out.println("第六步:使用创建好的 order 对象" + order);

        
        iocContainer.close();
    }

}

程序运行结果

image

3.5.3、读取 properties 文件

为什么要使用外部的 properties 文件

当 bean 的配置信息逐渐增多时,查找和修改一些 bean 的配置信息就变得愈加困难。这时可以将一部分信息提取到 bean 配置文件的外部,以 properties 格式的属性文件保存起来,同时在 bean 的配置文件中引用 properties 属性文件中的内容,从而实现一部分属性值在发生变化时仅修改 properties 属性文件即可。这种技术多用于连接数据库的基本信息的配置。

准备工作:引入数据库依赖

引入 druidmysql 的驱动

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.9</version>
</dependency>


<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.8</version>
</dependency>

直接数据库连接配置卸载 Spring 配置文件中

配置文件,指定数据库的用户名、用户密码、数据库连接地址、数据库驱动名称

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="username" value="root"/>
    <property name="password" value="root"/>
    <property name="url" value="jdbc:mysql:///test"/>
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
</bean>

测试代码

public class SpringTest {

    @Test
    public void test() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("spring-advance-config.xml");

        
        DataSource dataSource = (DataSource) iocContainer.getBean("dataSource");

        
        System.out.println(dataSource);
    }

}

程序运行结果

image

引用外部 properties 配置文件单独存放数据库配置信息

引入 context 名称空间

xmlns="http://www.springframework.org/schema/beans"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 复制,并将出现 beans 的位置全部替换为 context

<?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
                http://www.springframework.org/schema/context/spring-context.xsd">

image

代码测试

在类路径下创建 jdbc.properties 数据库配置文件

prop.userName=root
prop.password=root
prop.url=jdbc:mysql:///test
prop.driverClass=com.mysql.jdbc.Driver

通过 <context:property-placeholder> 标签中的 location 来制定配置文件的路径,classpath: 表示该配置文件位于类路径下,并通过 ${prop.userName} 的方式来取出配置文件中的属性值

<context:property-placeholder location="classpath:jdbc.properties"/>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="username" value="${prop.userName}"/>
    <property name="password" value="${prop.password}"/>
    <property name="url" value="${prop.url}"/>
    <property name="driverClassName" value="${prop.driverClass}"/>
</bean>

程序运行结果

image

3.5.4、bean 的自动装配

自动装配的概念

[1]手动装配:以 value 或 ref 的方式明确指定属性值都是手动装配。

[2]自动装配:根据指定的装配规则,不需要明确指定,Spring 自动将匹配的属性值注入 bean 中。

装配模式

[1]根据类型自动装配(byType):将类型匹配的 bean 作为属性注入到另一个 bean 中。若 IOC 容器中有多个与目标 bean 类型一致的 bean,Spring 将无法判定哪个 bean 最合适该属性,所以不能执行自动装配

[2]根据名称自动装配(byName):必须将目标 bean 的名称和属性名设置的完全相同

[3]通过构造器自动装配(constructor):当 bean 中存在多个构造器时,此种自动装配方式将会很复杂。不推荐使用。

选用建议

相对于使用注解的方式实现的自动装配,在 XML 文档中进行的自动装配略显笨拙,在项目中更多的使用注解的方式实现。

代码举例

通过 <bean> 标签的 autowire="byType",指定 student 对象中的 bean 按照类型进行装配

<bean id="student" class="com.oneby.entity.Student" autowire="byType">
    <property name="stuId" value="233"/>
    <property name="stuName" value="Oneby"/>
</bean>

<bean id="computer" class="com.oneby.entity.Computer">
    <property name="computerId" value="666"/>
    <property name="computerName" value="HP"/>
</bean>

程序运行结果

image

3.5.5、配置信息的继承

配置信息的继承

Spring 允许继承 bean 的配置,被继承的 bean 称为父 bean,继承这个父 bean 的 bean 称为子 bean

子 bean 从父 bean 中继承配置,包括 bean 的属性配置,子 bean 也可以覆盖从父 bean 继承过来的配置

准备工作:创建实体类

创建 CorporateSlave 类,其含义为社畜

public class CorporateSlave {

    private Integer id;
    private String name;
    private String company;
    private String hobby;
    private String profession;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getCompany() {
        return company;
    }

    public void setCompany(String company) {
        this.company = company;
    }

    public String getHobby() {
        return hobby;
    }

    public void setHobby(String hobby) {
        this.hobby = hobby;
    }

    public String getProfession() {
        return profession;
    }

    public void setProfession(String profession) {
        this.profession = profession;
    }

    @Override
    public String toString() {
        return "CorporateSlave{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", company='" + company + '\'' +
                ", hobby='" + hobby + '\'' +
                ", profession='" + profession + '\'' +
                '}';
    }
}

不使用继承配置 bean

喏,OnebyHeygo 两位社畜的 companyhobbyprofession 属性的值均相同,这样配置显得有些冗余

<bean id="corporateSlave1" class="com.oneby.entity.CorporateSlave">
    <property name="id" value="1"/>
    <property name="name" value="Oneby"/>
    
    <property name="company" value="OneTech"/>
    <property name="hobby" value="Code"/>
    <property name="profession" value="Programer"/>
</bean>

<bean id="corporateSlave2" class="com.oneby.entity.CorporateSlave">
    <property name="id" value="2"/>
    <property name="name" value="Heygo"/>
    
    <property name="company" value="OneTech"/>
    <property name="hobby" value="Code"/>
    <property name="profession" value="Programer"/>
</bean>

使用配置信息的继承配置 bean

配置信息的继承:Heygo 的配置信息继承于 Oneby(指定 bean 的 parent 属性),自然就获得了 Oneby 社畜的所有配置信息,只需要重写自己不一样的配置信息即可

<bean id="corporateSlave1" class="com.oneby.entity.CorporateSlave">
    <property name="id" value="1"/>
    <property name="name" value="Oneby"/>
    
    <property name="company" value="OneTech"/>
    <property name="hobby" value="Code"/>
    <property name="profession" value="Programer"/>
</bean>

<bean id="corporateSlave2" parent="corporateSlave1">
    
    <property name="id" value="2"/>
    <property name="name" value="Heygo"/>
</bean>

测试代码

public class SpringTest {

    @Test
    public void test() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("spring-advance-config.xml");

        
        CorporateSlave corporateSlave1 = (CorporateSlave) iocContainer.getBean("corporateSlave1");
        CorporateSlave corporateSlave2 = (CorporateSlave) iocContainer.getBean("corporateSlave2");

        
        System.out.println(corporateSlave1);
        System.out.println(corporateSlave2);
    }

}

程序运行结果

image

注意事项:配置信息的继承

父 bean 可以作为配置模板,也可以作为 bean 实例。若只想把父 bean 作为模板,可以设置<bean>abstract 属性为true,这样 Spring 将不会实例化这个 bean

3.5.6、bean 之间的依赖

bean 的作用域

有的时候创建一个 bean 的时候需要保证另外一个 bean 也被创建,这时我们称前面的 bean 对后面的 bean 有依赖。例如:要求创建 Student 对象的时候必须创建 Computer。这里需要注意的是依赖关系不等于引用关系,Student 即使依赖 Computer 也可以不引用它

举例一: student 对象依赖 computer 对象,但我们不创建 computer 对象

在配置文件呢中,我们只实例化 student 对象,并且执行其 depends-on 属性等于 computer,表示student 对象的创建依赖于 computer 对象的创建

<bean id="student" class="com.oneby.entity.Student" depends-on="computer">
    <property name="stuId" value="233" />
    <property name="stuName" value="Oneby" />
</bean>

测试代码

public class SpringTest {

    @Test
    public void test() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("spring-advance-config.xml");

        
        Student student = (Student) iocContainer.getBean("student");

        
        System.out.println(student);
    }

}

程序运行结果:org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'student' defined in class path resource [spring-advance-config.xml]: 'student' depends on missing bean 'computer'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'computer' available

报错信息已经很明显了:'student' depends on missing bean 'computer',说是缺少一个 computer 对象

举例二: student 对象依赖 computer 对象,我们就创建 computer 对象

既然 student 对象依赖 computer 对象,那么我们在配置文件中创建 computer 对象

<bean id="student" class="com.oneby.entity.Student" depends-on="computer">
    <property name="stuId" value="233"/>
    <property name="stuName" value="Oneby"/>
</bean>

<bean id="computer" class="com.oneby.entity.Computer"/>

程序运行结果:再次证明了以来不一定要引用

image

3.6、SpEL 表达式语言

3.6.1、SpEL 简介

SpEL 是什么?

SpEL 的全称是 Spring Expression Language,即 Spring 表达式语言,简称 SpEL,支持运行时查询并可以操作对象图,和 JSP 页面上的 EL 表达式、Struts2 中用到的 OGNL 表达式一样,SpEL 根据 JavaBean 风格的getXxx()setXxx()方法定义的属性访问对象图,完全符合我们熟悉的操作习惯。

3.6.2、SpEL 使用

0、基本语法

SpEL 使用#{…}作为定界符,所有在大框号中的字符都将被认为是 SpEL 表达式

1、字面量

  1. 整数:<property name="count" value="#{5}"/>

  2. 小数:<property name="frequency" value="#{89.7}"/>

  3. 科学计数法:<property name="capacity" value="#{1e4}"/>

  4. String 类型的字面量可以使用单引号或者双引号作为字符串的定界符号

    1. <property name="name" value="#{'Oneby'}"/>
    1. <property name='name' value='#{"Oneby"}'/>
  5. Boolean:<property name="enabled" value="#{false}"/>

2、引用其他 bean

<bean> 标签的 value 属性中通过 #{对象名} 引用其他 bean,注意:不能使用 ref 属性

<bean id="student" class="com.oneby.entity.Student">
    <property name="stuId" value="233"/>
    <property name="stuName" value="Oneby"/>
    <property name="computer" value="#{computer}"/>
</bean>

<bean id="computer" class="com.oneby.entity.Computer">
    <property name="computerId" value="666"/>
    <property name="computerName" value="HP"/>
</bean>

测试代码

public class SpringTest {

    @Test
    public void test() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("spel-test.xml");

        
        Student student = (Student) iocContainer.getBean("student");

        
        System.out.println(student);
    }

}

程序运行结果

image

3、引用其他 bean 的属性

<property> 标签中通过 #{对象名. 属性名} 引用其他 bean 的属性

<bean id="student" class="com.oneby.entity.Student">
    <property name="stuId" value="233"/>
    <property name="stuName" value="Oneby"/>
    <property name="computer" >
        <bean class="com.oneby.entity.Computer">
            <property name="computerId" value="#{computer.computerId}"/>
            <property name="computerName" value="#{computer.computerName}"/>
        </bean>
    </property>
</bean>

<bean id="computer" class="com.oneby.entity.Computer">
    <property name="computerId" value="666"/>
    <property name="computerName" value="HP"/>
</bean>

程序运行结果

image

4、调用非静态方法

通过 #{对象名. 方法名} 调用对象的非静态方法

<bean id="student" class="com.oneby.entity.Student">
    <property name="stuId" value="233"/>
    <property name="stuName" value="Oneby"/>
    <property name="computer">
        <bean class="com.oneby.entity.Computer">
            <property name="computerId" value="#{computer.getComputerId()}"/>
            <property name="computerName" value="#{computer.getComputerName()}"/>
        </bean>
    </property>
</bean>

<bean id="computer" class="com.oneby.entity.Computer">
    <property name="computerId" value="666"/>
    <property name="computerName" value="HP"/>
</bean>

程序运行结果

image

5、调用静态方法

定义获取随机整数的方法,随机整数的范围为 [start,end]

public class MathUtil {

    public static int getRandomInt(int start, int end) {
        return (int) (Math.random() * (end - start + 1) + start);
    }

}

通过 T(静态类路径). 方法名 调用静态方法

<bean id="student" class="com.oneby.entity.Student">
    <property name="stuId" value="#{T(com.oneby.util.MathUtil).getRandomInt(0,255)}"/>
    <property name="stuName" value="Oneby"/>
</bean>

程序运行结果

image

6、运算符

①算术运算符:+、-、*、/、%、^

②字符串连接:+

③比较运算符:<、>、==、<=、>=、lt、gt、eq、le、ge

④逻辑运算符:and, or, not, |

⑤三目运算符:判断条件? 判断结果为 true 时的取值: 判断结果为 false 时的取值

⑥正则表达式:matches

3.7、注解方式配置 bean

3.7.1、注解的概述

注解方式对比 XML 方式

相对于 XML 方式而言,通过注解的方式配置 bean 更加简洁和优雅,而且和 MVC 组件化开发的理念十分契合,是开发中常用的使用方式。

3.7.2、标识组件

用于标识 bean 的四个注解

①普通组件:@Component,用于标识一个受 Spring IOC 容器管理的组件

②持久化层组件:@Respository,用于标识一个受 Spring IOC 容器管理的持久化层组件

③业务逻辑层组件:@Service,用于标识一个受 Spring IOC 容器管理的业务逻辑层组件

④表述层控制器组件:@Controller,用于标识一个受 Spring IOC 容器管理的表述层控制器组件

注意:事实上 Spring 并没有能力识别一个组件到底是不是它所标记的类型,即使将@Respository注解用在一个表述层控制器组件上面也不会产生任何错误,所以@Respository@Service@Controller这几个注解仅仅是为了让开发人员自己明确当前的组件扮演的角色。

组件命名规则

[1]默认情况:使用组件的简单类名首字母小写后得到的字符串作为 bean 的id

[2]使用组件注解的value属性指定 bean 的id

3.7.3、扫描组件

引入 AOP 依赖

引入 AOP 相关依赖,不然开启组件扫描时会报错:org.springframework.beans.factory.BeanDefinitionStoreException: Unexpected exception parsing XML document from class path resource [annotation-config.xml]; nested exception is java.lang.NoClassDefFoundError: org/springframework/aop/TargetSource

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>

引入 context 名称空间

xmlns="http://www.springframework.org/schema/beans"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd 复制,并将出现 beans 的位置全部替换为 context

<?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
                http://www.springframework.org/schema/context/spring-context.xsd">

开启组件扫描

开启组件扫描,并指明要扫描的包路径

<context:component-scan base-package="com.oneby"/>

组件扫描的注意事项

组件扫描的详细说明

[1]base-package 属性指定一个需要扫描的基类包,Spring 容器将会扫描这个基类包及其子包中的所有类。

[2]当需要扫描多个包时可以使用逗号分隔。

[3]如果仅希望扫描特定的类而非基包下的所有类,可使用resource-pattern属性过滤特定的类,示例:

<context:component-scan 
    base-package="com.oneby" 
    resource-pattern="autowire/*.class"/>

包含与排除

  1. <context:include-filter>子节点表示要包含的目标类。注意:通常需要与use-default-filters属性配合使用才能够达到 “仅包含某些组件” 这样的效果。即:通过将use-default-filters属性设置为false,禁用默认过滤器,然后扫描的就只是include-filter中的规则指定的组件了。

  2. <context:exclude-filter>子节点表示要排除在外的目标类

  3. component-scan下可以拥有若干个include-filter和 exclude-filter 子节点

包扫描举例

<context:component-scan base-package="com.oneby" use-default-filters="false">
    <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan> 


<context:component-scan base-package="com.oneby">
    <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

过滤表达式

image

3.7.4、组件装配

组件装配的说明

项目中组件装配的需求

Controller 组件中往往需要用到 Service 组件的实例,Service 组件中往往需要用到 Repository 组件的实例。Spring 可以通过注解的方式帮我们实现属性的装配。

组件扫描的原理

在指定要扫描的包时,<context:component-scan> 元素会自动注册一个 bean 的后置处理器:AutowiredAnnotationBeanPostProcessor的实例。该后置处理器可以自动装配标记了@Autowired@Resource@Inject注解的属性

@Autowired注解

[1]根据类型实现自动装配。

[2]构造器、普通字段 (即使是非public)、一切具有参数的方法都可以应用@Autowired注解

[3]默认情况下,所有使用@Autowired注解的属性都需要被设置。当 Spring 找不到匹配的 bean 装配属性时,会抛出异常。

[4]若某一属性允许不被设置,可以设置@Autowired注解的required属性为 false

[5]默认情况下,当 IOC 容器里存在多个类型兼容的 bean 时,Spring 会尝试匹配 bean 的id值是否与变量名相同,如果相同则进行装配。如果 bean 的id值不相同,通过类型的自动装配将无法工作。此时可以在@Qualifier注解里提供 bean 的名称。Spring 甚至允许在方法的形参上标注@Qualifiter注解以指定注入 bean 的名称。

[6]@Autowired注解也可以应用在数组类型的属性上,此时 Spring 将会把所有匹配的 bean 进行自动装配。

[7]@Autowired注解也可以应用在集合属性上,此时 Spring 读取该集合的类型信息,然后自动装配所有与之兼容的 bean。

[8]@Autowired注解用在java.util.Map上时,若该Map的键值为String,那么 Spring 将自动装配与值类型兼容的 bean 作为值,并以 bean 的id值作为键。

@Qualifier注解

通过类型的自动装配将无法工作。此时可以在@Qualifier注解里提供 bean 的名称,@Qualifier注解需要和上面@Autowired注解一起使用

@Autowired 
@Qualifier(value = "orderDao1") 
private OrderDao orderDao;

@Resource注解

@Resource注解要求提供一个 bean 名称的属性,若该属性为空,则自动采用标注处的变量或方法名作为 bean 的名称。@Resource是 JDK 提供的注解,咱还是尽量使用 Spring 提供的注解吧~

解释上面那句话:如果使用@Resource则表示按照类型进行注入,我觉得等同于@Autowire的效果吧;如果使用@Resource(name="Xxx")则表示根据 bean 的名称进行注入

@Resource(name = "orderDao1") 
private OrderDao orderDao;

@Inject注解

@Inject@Autowired注解一样也是按类型注入匹配的 bean,但没有 reqired 属性。奇怪了,难道 Spring 5.2.6 版本该注解被移除了吗?

@Value注解

@Value注解用于注入普通属性的值,比如@Value(value = "Oneby")表示将"Oneby"字符串注入到属性中

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

组件装配的代码示例

DAO 层推荐使用 @Repository 注解标识 bean

@Repository
public class OrderDao {

    public void sell() {
        System.out.println("DAO 层操作:商品库存减一");
    }

}

Service 层推荐使用 @Service 注解标识 bean,并通过 @Autowired 注解标识

@Service
public class OrderService {

    @Autowired
    private OrderDao orderDao;

    public void sell() {
        orderDao.sell();
        System.out.println("Service 层操作:出售一件商品");
    }

}

测试代码

public class SpringTest {

    @Test
    public void test() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("annotation-config.xml");

        
        OrderService orderService = (OrderService) iocContainer.getBean("orderService");

        
        orderService.sell();
    }

}

程序运行结果

image

3.7.5、完全注解开发

创建 Spring 配置类

创建 SpringConfig 配置类,代替之前的 XML 配置文件

  1. @Configuration 标识这是一个配置类

  2. @ComponentScan(basePackages = "com.oneby") 配置组件扫描

  3. @Bean 用于向 IOC 容器中注入一个普通的 bean

@Configuration
@ComponentScan(basePackages = "com.oneby")
public class SpringConfig {

    @Bean
    public OrderService getOrderService(){
        return new OrderService();
    }

}

完全注解的代码测试

测试代码:这次需要 new 一个 AnnotationConfigApplicationContext 对象,并传入配置类的类型

@Test
public void testCompleteAnnotation() {
    
    ApplicationContext iocContainer =
            new AnnotationConfigApplicationContext(SpringConfig.class);

    
    OrderService orderService = (OrderService) iocContainer.getBean("orderService");

    
    orderService.sell();
}

程序运行结果

image

3.8、泛型依赖注入

泛型依赖注入的概述

Spring 4.x 中可以为子类注入子类对应的泛型类型的成员变量的引用

image

泛型依赖注入的实现

组件基类

BaseRepository
public class BaseRepository<T> {
    
    public void save() {
        System.out.println("Saved by BaseRepository");
    }

}
BaseService
public class BaseService<T> {
    
    @Autowired
    private BaseRepository<T> repository;
    
    public void add() {
        repository.save();
    }

}

组件实体类

UserRepository
@Repository
public class UserRepository extends BaseRepository<User>{
    
    public void save() {
        System.out.println("Saved by UserRepository");
    }

}
UserService
@Service
public class UserService extends BaseService<User>{

}

模型实体类

User
public class User {

}

测试

测试代码

@Test
public void test() {
    ApplicationContext ioc = new ClassPathXmlApplicationContext("di.xml");
    UserService us = (UserService) ioc.getBean("userService");
    us.add();
}

执行结果

Saved by UserRepository

PS:看球不懂

3.9、配置文件整合

Spring 可以很方便地整合其他配置文件

  1. Spring 允许通过<import>将多个配置文件引入到一个文件中,进行配置文件的集成。这样在启动 Spring 容器时,仅需要指定这个合并好的配置文件就可以。

  2. import元素的resource属性支持 Spring 的标准的路径资源

image

4、AOP

4.1、AOP 前奏

提出问题:数学计算器案例

计算器要求

①执行加减乘除运算

②日志:在程序执行期间追踪正在发生的活动

③验证:希望计算器只能处理正数的运算

计算器的常规实现

UML 类图

image

计算器的常规实现代码(这里为了形参类型为 int,无伤大雅)

image

常规实现存在的问题

代码混乱:越来越多的非业务需求(日志和验证等)加入后,原有的业务方法急剧膨胀。每个方法在处理核心逻辑的同时还必须兼顾其他多个关注点。

代码分散:以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块(方法)里多次重复相同的日志代码。如果日志需求发生变化,必须修改所有模块。

动态代理的介绍

代理设计模式的原理:使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。代理对象决定是否以及何时将方法调用转到原始对象上。

image

数学计算器的改进

日志处理器

image

验证处理器

image

测试代码

image

4.2、AOP 概述

AOP 概述

  1. AOP(Aspect-Oriented Programming,面向切面编程):是一种新的方法论,是对传统 OOP(Object-Oriented Programming,面向对象编程) 的补充。

  2. AOP 编程操作的主要对象是切面 (aspect),而切面模块化横切关注点。

  3. 在应用 AOP 编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为 “切面”。

  4. AOP 的好处:每个事物逻辑位于一个位置,代码不分散,便于维护和升级;业务模块更简洁,只包含核心业务代码

image

用通俗的话将:面向切面编程(方面),利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,可在不通过修改源代码方式,在主干功能里面添加新功能

AOP 相关术语

看不懂的专业术语

通知(Advice):就是你想要的功能,也就是上面说的日志处理、验证处理等。你给先定义好把,然后在想用的地方用一下。

连接点(JoinPoint):这个更好解释了,就是 Spring 允许你使用通知的地方,那可真就多了,基本每个方法的前,后(两者都有也行),或抛出异常时都可以是连接点,spring 只支持方法连接点. 其他如 aspectJ 还可以让你在构造器或属性注入时都行,不过那不是咱关注的,只要记住,和方法有关的前前后后(抛出异常),都是连接点。

切入点(Pointcut):上面说的连接点的基础上,来定义切入点,你的一个类里,有 15 个方法,那就有几十个连接点了对把,但是你并不想在所有方法附近都使用通知(使用叫织入,以后再说),你只想让其中的几个,在调用这几个方法之前,之后或者抛出异常时干点什么,那么就用切点来定义这几个方法,让切点来筛选连接点,选中那几个你想要的方法。

切面(Aspect):切面是通知和切入点的结合。现在发现了吧,没连接点什么事情,连接点就是为了让你好理解切入点,搞出来的,明白这个概念就行了。通知说明了干什么和什么时候干(什么时候通过方法名中的 before,after,around 等就能知道),而切入点说明了在哪干(指定到底是哪个方法),这就是一个完整的切面定义。

引入(introduction):允许我们向现有的类添加新方法属性。这不就是把切面(也就是新方法属性:通知定义的)用到目标类中吗

目标(target):引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑,他可以在毫不知情的情况下,被咱们织入切面。而自己专注于业务本身的逻辑。

代理(proxy):怎么实现整套 aop 机制的,都是通过代理。

织入(weaving):把切面应用到目标对象来创建新的代理对象的过程。有 3 种方式,spring 采用的是运行时。

image

用通俗的话讲

连接点:类里面哪些方法可以被增强,这些方法被称为连接点

切入点:实际被真正增强的方法,称为切入点

通知(增强):实际增强的逻辑部分称为通知(增强)。通知的类型:前置通知、后置通知、环绕通知、异常通知、最终通知

切面:把通知应用到切入点过程(是动作)

AspectJ 简介

AspectJ:Java 社区里最完整最流行的 AOP 框架。在 Spring2.0 以上版本中,可以使用基于 AspectJ 注解或基于 XML 配置的 AOP。

在 Spring 中使用 AspectJ 进行 AOP 操作

实现 AOP 操作的步骤

编写切面类(通过 @Aspect 注解标识这是一个切面类),并且不要忘记将切面类交给 Spring IOC 管理(Component 注解),并编写相应的通知方法与切入点表达式

在 Spring 配置文件中开启 aop 功能:通过 <aop:aspectj-autoproxy/> 注解开启 aop 功能。当 Spring IOC 容器侦测到 bean 配置文件中的<aop:aspectj-autoproxy>元素时,会自动为与 AspectJ 切面匹配的 bean 创建代理

AspectJ 支持 5 种类型的通知注解

[1]@Before:前置通知,在方法执行之前执行

[2]@After:后置通知,在方法执行之后执行

[3]@AfterRunning:返回通知,在方法返回结果之后执行

[4]@AfterThrowing:异常通知,在方法抛出异常之后执行

[5]@Around:环绕通知,围绕着方法执行

4.3、AOP 细节

准备工作:在 Spring 中使用 AspectJ

引入 maven 依赖:引入 aop 和 aspects 相关的依赖

<!-- spring-aop -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>

<!-- spring-aspects -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>

<!-- aspectjweaver -->
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.6</version>
</dependency>

<!-- aopalliance -->
<dependency>
    <groupId>aopalliance</groupId>
    <artifactId>aopalliance</artifactId>
    <version>1.0</version>
</dependency>

<!-- cglib -->
<dependency>
    <groupId>net.sourceforge.cglib</groupId>
    <artifactId>com.springsource.net.sf.cglib</artifactId>
    <version>2.2.0</version>
</dependency>

编写 Spring 配置文件:引入 contextaop 名称空间;开启组件扫描,并指明包路径;开启自动代理功能

<?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: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
                http://www.springframework.org/schema/context/spring-context.xsd
                http://www.springframework.org/schema/aop
                http://www.springframework.org/schema/aop/spring-aop.xsd">

    
    <context:component-scan base-package="com.oneby"/>

    
    <aop:aspectj-autoproxy/>

</beans>

ArithmeticCalculator 接口:定义各种数学运算方法

public interface ArithmeticCalculator {

    void add(int i, int j);

    void sub(int i, int j);
    
    void mul(int i, int j);

    void div(int i, int j);

}

ArithmeticCalculatorImpl 类:实现了 ArithmeticCalculator 接口中各种抽象的数学运算方法

@Component
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
    @Override
    public void add(int i, int j) {
        int result = i + j;
        System.out.println("计算器计算得到的结果为: " + result);
    }

    @Override
    public void sub(int i, int j) {
        int result = i - j;
        System.out.println("计算器计算得到的结果为: " + result);
    }

    @Override
    public void mul(int i, int j) {
        int result = i * j;
        System.out.println("计算器计算得到的结果为: " + result);
    }

    @Override
    public void div(int i, int j) {
        int result = i / j;
        System.out.println("计算器计算得到的结果为: " + result);
    }
}

切入点表达式的相关细节

切入点的作用:通过表达式的方式定位一个或多个具体的连接点(哪些方法需要被增强)

切入点表达式的语法格式execution([权限修饰符] [返回值类型] [简单类名 / 全类名] [方法名]([参数列表]))

切入点表达式的举例一

表达式:execution(* com.atguigu.spring.ArithmeticCalculator.*(..))

含义:增强 ArithmeticCalculator 接口中声明的所有方法

解释说明:第一个 “” 代表任意修饰符及任意返回值;第二个 “” 代表任意方法;“…” 匹配任意数量、任意类型的参数

注:若目标类、接口与该切面类在同一个包中可以省略包名

切入点表达式的举例二

表达式:execution(public * ArithmeticCalculator.*(..))

含义: 增强 ArithmeticCalculator 接口的所有公有方法(TMD 接口中的方法不都是 public 吗)

切入点表达式的举例三

表达式:execution(public double ArithmeticCalculator.*(..))

含义:增强 ArithmeticCalculator 接口中返回 double 类型数值的方法

切入点表达式的举例四

表达式:execution(public double ArithmeticCalculator.*(double, ..))

含义:第一个参数为 double 类型的方法。“…” 匹配任意数量、任意类型的参数

切入点表达式的举例五

表达式: execution(public double ArithmeticCalculator.*(double, double))

含义:参数类型为 double,double 类型的方法

切入点表达式的举例六:在 AspectJ 中,切入点表达式可以通过 “&&”、“||”、“!” 等操作符结合起来。

表达式:execution (* *.add(int,..)) || execution(* *.sub(int,..))

含义:任意类中第一个参数为 int 类型的 add 方法或 sub 方法

将切入点表达式应用到实际的切面类中

image

当前连接点的相关细节

切入点表达式通常都会是从宏观上定位一组方法,和具体某个通知的注解结合起来就能够确定对应的连接点。

那么就一个具体的连接点而言,我们可能会关心这个连接点的一些具体信息,例如:当前连接点所在方法的方法名、当前传入的参数值等等。这些信息都封装在JoinPoint接口的实例对象中

image

五种通知的相关细节

通知的概述

  1. 在具体的连接点上要执行的操作。

  2. 一个切面可以包括一个或者多个通知。

  3. 通知所使用的注解的值往往就是切入点表达式

前置通知

前置通知:在方法执行之前执行的通知,使用@Before注解

后置通知

后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的时候,使用@After注解

返回通知

返回通知:无论连接点是正常返回还是抛出异常,后置通知都会执行。如果只想在连接点返回的时候记录日志,应使用返回通知代替后置通知。使用@AfterReturning注解

在返回通知中访问连接点的返回值

  1. 在返回通知中,只要将 returning 属性添加到@AfterReturning注解中,就可以访问连接点的返回值。该属性的值即为用来传入返回值的参数名称

  2. 必须在通知方法的签名中添加一个同名参数。在运行时 Spring AOP 会通过这个参数传递返回值

  3. 原始的切点表达式需要出现在pointcut属性中

image

异常通知

异常通知:只在连接点抛出异常时才执行异常通知

throwing属性添加到@AfterThrowing注解中,也可以访问连接点抛出的异常。Throwable 是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。

如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行

image

环绕通知

环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。

对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。

在环绕通知中需要明确调用ProceedingJoinPointproceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。

注意:环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。

image

4.4、AOP 注解方式

准备工作:在 Spring 中使用 AspectJ

引入 maven 依赖:引入 aop 和 aspects 相关的依赖

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>


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


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


<dependency>
    <groupId>aopalliance</groupId>
    <artifactId>aopalliance</artifactId>
    <version>1.0</version>
</dependency>


<dependency>
    <groupId>net.sourceforge.cglib</groupId>
    <artifactId>com.springsource.net.sf.cglib</artifactId>
    <version>2.2.0</version>
</dependency>

编写 Spring 配置文件:引入 contextaop 名称空间;开启组件扫描,并指明包路径;开启自动代理功能

<?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: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
                http://www.springframework.org/schema/context/spring-context.xsd
                http://www.springframework.org/schema/aop
                http://www.springframework.org/schema/aop/spring-aop.xsd">

    
    <context:component-scan base-package="com.oneby"/>

    
    <aop:aspectj-autoproxy/>

</beans>

目标类的定义

ArithmeticCalculator 接口:定义各种数学运算方法

public interface ArithmeticCalculator {

    void add(int i, int j);

    void sub(int i, int j);
    
    void mul(int i, int j);

    void div(int i, int j);

}

ArithmeticCalculatorImpl 类:实现了 ArithmeticCalculator 接口中各种抽象的数学运算方法

@Component
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
    @Override
    public void add(int i, int j) {
        int result = i + j;
        System.out.println("计算器计算得到的结果为: " + result);
    }

    @Override
    public void sub(int i, int j) {
        int result = i - j;
        System.out.println("计算器计算得到的结果为: " + result);
    }

    @Override
    public void mul(int i, int j) {
        int result = i * j;
        System.out.println("计算器计算得到的结果为: " + result);
    }

    @Override
    public void div(int i, int j) {
        int result = i / j;
        System.out.println("计算器计算得到的结果为: " + result);
    }
}

切面类的定义

  1. @Aspect 注解标识这是一个切面类

  2. @Component 注解将这个切面类对象交由 Spring IOC 进行管理

  3. execution(* com.oneby.calc.ArithmeticCalculator.*(..)) 表示增强 ArithmeticCalculator 接口中的所有方法

@Component
@Aspect
public class CalculatorLoggingAspect {

    @Before(value = "execution(* com.oneby.calc.ArithmeticCalculator.*(..))")
    public void before(JoinPoint joinPoint) {
        System.out.println("@Before 前置通知");
    }

    @After(value = "execution(* com.oneby.calc.ArithmeticCalculator.*(..))")
    public void after(JoinPoint joinPoint) {
        System.out.println("@After 后置通知");
    }

    @AfterReturning(value = "execution(* com.oneby.calc.ArithmeticCalculator.*(..))")
    public void afterReturning(JoinPoint joinPoint) {
        System.out.println("@AfterReturning 返回后通知");
    }

    @AfterThrowing(value = "execution(* com.oneby.calc.ArithmeticCalculator.*(..))")
    public void afterThrowing(JoinPoint joinPoint) {
        System.out.println("@AfterThrowing 异常通知");
    }

    @Around(value = "execution(* com.oneby.calc.ArithmeticCalculator.*(..))")
    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("@Around 环绕通知之前");
        proceedingJoinPoint.proceed(); 
        System.out.println("@Around 环绕通知之后");
    }
    
}

代码测试

测试代码

public class SpringTest {

    @Test
    public void test() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("aop-test.xml");

        
        ArithmeticCalculator arithmeticCalculator = iocContainer.getBean(ArithmeticCalculator.class);

        
        System.out.println("spring版本:" + SpringVersion.getVersion() + "下的测试");
        arithmeticCalculator.add(1, 1);
    }

}

程序运行结果:我嘞个去,之前测试过 Spring 的通知执行顺序:第 4 章 Spring,不是说 Spring5 下的通知改进了吗?已经和 Spring4 下的通知执行顺序不一样了,咋还是和 Spring 的通知顺序一样

image

Spring 5.2.8 版本下的测试

还好之前跟着阳哥测试了一波,我将 Spring 版本改为 5.2.8 后,通知的执行顺序就正常啦:环绕通知包裹住其他通知;并且 @Atfer@AfterReturning 之后执行

image

异常情况下的通知执行顺序

测试代码

public class SpringTest {

    @Test
    public void test() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("aop-test.xml");

        
        ArithmeticCalculator arithmeticCalculator = iocContainer.getBean(ArithmeticCalculator.class);

        
        System.out.println("spring版本:" + SpringVersion.getVersion() + "下的测试");
        arithmeticCalculator.div(1, 0);


    }

}

程序运行结果:首先目标方法没有执行;没有 @AtferReturning 通知,之前 @AtferReturning 通知的地方变为了 @AtferThrowingAround 后置环绕通知也没有执行

image

完全使用注解方式进行 aop 开发

创建 SpringAopConfig 配置类:①@Configuration 表示这是一个配置类;②@ComponentScan(basePackages = "com.oneby") 配置包扫描路径为 com.oneby;③@EnableAspectJAutoProxy(proxyTargetClass = true) 表示开启 AOP 自动代理

@Configuration
@ComponentScan(basePackages = "com.oneby")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class SpringAopConfig {
    
}

测试代码

public class SpringTest {

    @Test
    public void testAnnotation() {
        
        ApplicationContext iocContainer =
                new AnnotationConfigApplicationContext(SpringAopConfig.class);

        
        ArithmeticCalculator arithmeticCalculator = iocContainer.getBean(ArithmeticCalculator.class);

        
        System.out.println("spring版本:" + SpringVersion.getVersion() + "下的测试");
        arithmeticCalculator.div(1, 0);
    }

}

程序运行结果

image

4.5、AOP 进阶操作

重用切入点定义

在编写 AspectJ 切面时,可以直接在通知注解中书写切入点表达式。但同一个切点表达式可能会在多个通知中重复出现。

在 AspectJ 切面中,可以通过@Pointcut注解将一个切入点声明成简单的方法。切入点的方法体通常是空的。

切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。

其他通知可以通过方法名称引入该切入点

image

指定切面的优先级

在同一个连接点上应用不止一个切面时,除非明确指定,否则它们的优先级是不确定的。

切面的优先级可以通过实现Ordered接口或利用@Order注解指定。

实现Ordered接口,getOrder()方法的返回值越小,优先级越高。

若使用@Order注解,序号出现在注解中

@Component
@Aspect
@Order(0)
public class CalculatorValidationAspect {
    
@Component
@Aspect
@Order(1)
public class CalculatorLoggingAspect {

4.6、AOP XML 方式

XML 配置概述

除了使用 AspectJ 注解声明切面,Spring 也支持在 bean 配置文件中声明切面。这种声明是通过 aop 名称空间中的 XML 元素完成的。

正常情况下,基于注解的声明要优先于基于 XML 的声明。通过 AspectJ 注解,切面可以与 AspectJ 兼容,而基于 XML 的配置则是 Spring 专有的。由于 AspectJ 得到越来越多的 AOP 框架支持,因此以注解风格编写的切面将会有更多重用的机会。

XML 配置细节

配置切面

在 bean 配置文件中,所有的 Spring AOP 配置都必须定义在<aop:config>元素内部。对于每个切面而言,都要创建一个<aop:aspect>元素来为具体的切面实现引用后端 bean 实例。切面 bean 必须有一个标识符,供<aop:aspect>元素引用。

image

声明切入点

  1. 切入点使用<aop:pointcut>元素声明。

  2. 切入点必须定义在<aop:aspect>元素下,或者直接定义在<aop:config>元素下。

  3. 定义在<aop:aspect>元素下:只对当前切面有效

  4. 定义在<aop:config>元素下:对所有切面都有效

  5. 基于 XML 的 AOP 配置不允许在切入点表达式中用名称引用其他切入点。

image

声明通知

  1. 在 aop 名称空间中,每种通知类型都对应一个特定的 XML 元素。

  2. 通知元素需要使用<pointcut-ref>来引用切入点,或用<pointcut>直接嵌入切入点表达式。

  3. method属性指定切面类中通知方法的名称

image

XML 配置细节

XML 配置文件

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

    
    <aop:aspectj-autoproxy/>

    
    <bean id="arithmeticCalculatorImpl" class="com.oneby.calc.ArithmeticCalculatorImpl"/>
    <bean id="calculatorLoggingAspect" class="com.oneby.calc.CalculatorLoggingAspect"/>

    
    <aop:config>
        
        <aop:pointcut id="calcPointcut" expression="execution(* com.oneby.calc.ArithmeticCalculator.*(..))"/>
        
        <aop:aspect ref="calculatorLoggingAspect">
            
            <aop:before method="before" pointcut-ref="calcPointcut"/>
            <aop:after method="after" pointcut-ref="calcPointcut"/>
            <aop:after-returning method="afterReturning" pointcut-ref="calcPointcut"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="calcPointcut"/>
            <aop:around method="around" pointcut-ref="calcPointcut"/>
        </aop:aspect>
    </aop:config>

</beans>

测试代码

public class SpringTest {

    @Test
    public void test() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("aop-test.xml");

        
        ArithmeticCalculator arithmeticCalculator = iocContainer.getBean(ArithmeticCalculator.class);

        
        System.out.println("spring版本:" + SpringVersion.getVersion() + "下的测试");
        arithmeticCalculator.div(1, 0);
    }

    @Test
    public void testAnnotation() {
        
        ApplicationContext iocContainer =
                new AnnotationConfigApplicationContext(SpringAopConfig.class);

        
        ArithmeticCalculator arithmeticCalculator = iocContainer.getBean(ArithmeticCalculator.class);

        
        System.out.println("spring版本:" + SpringVersion.getVersion() + "下的测试");
        arithmeticCalculator.div(1, 0);
    }

    @Test
    public void testXml() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("aop-complete-xml.xml");

        
        ArithmeticCalculator arithmeticCalculator = iocContainer.getBean(ArithmeticCalculator.class);

        
        System.out.println("spring版本:" + SpringVersion.getVersion() + "下的测试");
        arithmeticCalculator.div(1, 0);
    }

}

程序运行结果

image

5、JdbcTemplate

5.1、概述

JDBC 概述

为了使 JDBC 更加易于使用,Spring 在 JDBC API 上定义了一个抽象层,以此建立一个 JDBC 存取框架。

作为 Spring JDBC 框架的核心,JDBC 模板的设计目的是为不同类型的 JDBC 操作提供模板方法,通过这种方式,可以在尽可能保留灵活性的情况下,将数据库存取的工作量降到最低。

可以将 Spring 的JdbcTemplate看作是一个小型的轻量级持久化层框架,和我们之前使用过的 DBUtils 风格非常接近。

5.2、环境准备

引入依赖

引入 jdbcmysql 的相关依赖

<!-- spring-jdbc -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

<!-- spring-tx -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

<!-- spring-orm -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

<!-- druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.9</version>
</dependency>

<!-- mysql -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.8</version>
</dependency>

编写配置文件

将数据库连接信息抽取到 jdbc.properties 配置文件中

prop.userName=root
prop.password=root
prop.url=jdbc:mysql:///test
prop.driverClass=com.mysql.jdbc.Driver

Spring 配置文件

  1. 因为我们会用到 @Repository@Service@Controller 之类的注解向 IOC 容器注入 bean 实例,因此需要开启组件扫描

  2. 使用 <context:property-placeholder> 标签指定 jdbc.properties 配置文件的路径,并取出其中的配置,设置数据库连接池的相关属性:用户名 username、用户密码 password、数据库连接地址 url、驱动类名 driverClassName

  3. 使用 JdbcTemplate 对象进行持久化操作,需要为其注入数据源 dataSource

<?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
                http://www.springframework.org/schema/context/spring-context.xsd">

    
    <context:component-scan base-package="com.oneby"/>

    
    
    <context:property-placeholder location="classpath:jdbc.properties"/>
    
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="username" value="${prop.userName}"/>
        <property name="password" value="${prop.password}"/>
        <property name="url" value="${prop.url}"/>
        <property name="driverClassName" value="${prop.driverClass}"/>
    </bean>

    
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

</beans>

准备数据库表

book_id 为图书编号,交由数据库维护;book_name 为书名;book_category 为书籍所属类别

image

创建实体类

Book 实体类

public class Book {

    private Integer bookId;
    private String bookName;
    private String bookCategory;

    public Book() {
    }

    public Book(String bookName, String bookCategory) {
        this.bookName = bookName;
        this.bookCategory = bookCategory;
    }

    public Book(Integer bookId, String bookName, String bookCategory) {
        this.bookId = bookId;
        this.bookName = bookName;
        this.bookCategory = bookCategory;
    }

    public Integer getBookId() {
        return bookId;
    }

    public void setBookId(Integer bookId) {
        this.bookId = bookId;
    }

    public String getBookName() {
        return bookName;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

    public String getBookCategory() {
        return bookCategory;
    }

    public void setBookCategory(String bookCategory) {
        this.bookCategory = bookCategory;
    }

    @Override
    public String toString() {
        return "Book{" +
                "bookId=" + bookId +
                ", bookName='" + bookName + '\'' +
                ", bookCategory='" + bookCategory + '\'' +
                '}';
    }

}

5.3、持久化操作

增删改

增删改用这个函数:public int update(String sql, @Nullable Object... args) throws DataAccessException,通过 sql 指明要执行的 SQL 语句,并通过可变长参数 args 指明 SQL 语句的参数

image

查询某个值或对象(查询单行)

查询某个值或对象用这个函数:public <T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException,通过 sql 指明要执行的 SQL 语句,通过 RowMapper 对象指明从数据库查询出来的参数应该如何封装到指定的对象中,并通过可变长参数 args 指明 SQL 语句的参数

image

RowMapper 接口

RowMapper 接口是一个函数式接口,其中只有一个方法:T mapRow(ResultSet rs, int rowNum) throws SQLException,该方法的具体作用是将查询得到的每行数据映射到 ResultSet

image

BeanPropertyRowMapper

BeanPropertyRowMapper 类实现了 RowMapper 接口,其功能是:将查询得到的结果集的值,注入到对象属性中

查询对象集合(查询多行)

查询对象集合使用这个函数:public <T> List<T> query(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException,通过 sql 指明要执行的 SQL 语句,通过 RowMapper 对象指明从数据库查询出来的参数应该如何封装到指定的对象中,并通过可变长参数 args 指明 SQL 语句的参数

image

批量操作

批量操作包括批量增加、批量更新、批量删除操作,这些操作都可以使用 public int[] batchUpdate(String sql, List<Object[]> batchArgs) throws DataAccessException 函数完成:通过 sql 指明要执行的 SQL 语句,并通过参数 batchArgs 指明批处理 SQL 语句的参数

image

5.4、持久化代码

使用 JdbcTemplate 操作数据库,进行持久化操作

BookDao 接口的定义

public interface BookDao {

    
    public int addBook(Book book);

    
    public int deleteBook(String bookId);

    
    public int updateBook(Book book);

    
    public Book findBookInfo(int bookId);

    
    public int findBookCount();

    
    public List<Book> findAllBookInfo();

    
    public int[] batchAddBook(List<Book> books);

    
    public int[] batchUpdateBook(List<Book> books);

    
    public int[] batchDeleteBook(List<Integer> bookId);
    
}

BookDaoImpl 实现类的定义

  1. 增删改使用 update() 方法

  2. 查询某个值或对象 queryForObject() 方法

  3. 查询对象集合使用 query() 方法

  4. 批量处理使用 batchUpdate() 方法

@Repository
public class BookDaoImpl implements BookDao {

    
    @Autowired
    private JdbcTemplate jdbcTemplate;


    @Override
    public int addBook(Book book) {
        
        String sql = "insert into t_books (book_name, book_category) values (?, ?) ";

        
        Object[] args = {book.getBookName(), book.getBookCategory()};

        
        int insertRows = jdbcTemplate.update(sql, args);
        return insertRows;
    }

    @Override
    public int deleteBook(String bookId) {
        
        String sql = "delete from t_books where book_id = ?";

        
        int deleteRows = jdbcTemplate.update(sql, bookId);
        return deleteRows;
    }

    @Override
    public int updateBook(Book book) {
        
        String sql = "update t_books set book_name = ?, book_category = ? where book_id = ?";

        
        Object[] args = {book.getBookName(), book.getBookCategory(), book.getBookId()};

        
        int insertRows = jdbcTemplate.update(sql, args);
        return insertRows;
    }

    @Override
    public Book findBookInfo(int bookId) {
        
        String sql = "select * from t_books where book_id = ?";

        
        Book book = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Book.class), bookId);
        return book;
    }

    @Override
    public int findBookCount() {
        
        String sql = "select count(*) from t_books";

        
        Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
        return count;
    }

    @Override
    public List<Book> findAllBookInfo() {
        
        String sql = "select * from t_books";

        
        List<Book> books = jdbcTemplate.query(sql, new BeanPropertyRowMapper<Book>(Book.class));
        return books;
    }

    @Override
    public int[] batchAddBook(List<Book> books) {
        
        String sql = "insert into t_book (book_name, book_category) values (?, ?)";

        
        List<Object[]> batchArgs = new ArrayList<>();
        for (Book book : books) {
            batchArgs.add(new Object[]{book.getBookName(), book.getBookCategory()});
        }

        
        int[] batchAffectedRows = jdbcTemplate.batchUpdate(sql, batchArgs);
        return batchAffectedRows;
    }

    @Override
    public int[] batchUpdateBook(List<Book> books) {
        
        String sql = "update t_books set book_name = ?, book_category = ? where book_id = ?";

        
        List<Object[]> batchArgs = new ArrayList<>();
        for (Book book : books) {
            batchArgs.add(new Object[]{book.getBookName(), book.getBookCategory(), book.getBookId()});
        }

        
        int[] batchAffectedRows = jdbcTemplate.batchUpdate(sql, batchArgs);
        return batchAffectedRows;
    }

    @Override
    public int[] batchDeleteBook(List<Integer> bookIds) {
        
        String sql = "delete from t_books where book_id = ?";

        
        List<Object[]> batchArgs = new ArrayList<>();
        for (Integer bookId : bookIds) {
            batchArgs.add(new Object[]{bookId});
        }

        
        int[] batchAffectedRows = jdbcTemplate.batchUpdate(sql, batchArgs);
        return batchAffectedRows;
    }

}

6、声明式事务管理

6.1、事务概述

事务概述与其 ACID 属性

在 JavaEE 企业级开发的应用领域,为了保证数据的完整性和一致性,必须引入数据库事务的概念,所以事务管理是企业级应用程序开发中必不可少的技术。

事务就是一组由于逻辑上紧密关联而合并成一个整体 (工作单元) 的多个数据库操作,这些操作要么都执行,要么都不执行。

事务的四个关键属性 (ACID)

原子性 (atomicity):“原子” 的本意是 “不可再分”,事务的原子性表现为一个事务中涉及到的多个操作在逻辑上缺一不可。事务的原子性要求事务中的所有操作要么都执行,要么都不执行。

一致性 (consistency):“一致” 指的是数据的一致,具体是指:所有数据都处于满足业务规则的一致性状态。一致性原则要求:一个事务中不管涉及到多少个操作,都必须保证事务执行之前数据是正确的,事务执行之后数据仍然是正确的。如果一个事务在执行的过程中,其中某一个或某几个操作失败了,则必须将其他所有操作撤销,将数据恢复到事务执行之前的状态,这就是回滚。

隔离性 (isolation):在应用程序实际运行过程中,事务往往是并发执行的,所以很有可能有许多事务同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。隔离性原则要求多个事务在并发执行过程中不会互相干扰。

持久性 (durability):持久性原则要求事务执行完成后,对数据的修改永久的保存下来,不会因各种系统错误或其他意外情况而受到影响。通常情况下,事务对数据的修改应该被写入到持久化存储器中。

6.2、Spring 事务管理

之前的方式:编程式事务管理

使用原生的 JDBC API 进行事务管理

  1. 获取数据库连接 Connection 对象

  2. 取消事务的自动提交

  3. 执行操作

  4. 正常完成操作时手动提交事务

  5. 执行失败时回滚事务

  6. 关闭相关资源

编程式事务管理的缺点

使用原生的 JDBC API 实现事务管理是所有事务管理方式的基石,同时也是最典型的编程式事务管理。编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务的提交和回滚。在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务管理代码。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余

现在的方式:声明式事务管理

大多数情况下声明式事务比编程式事务管理更好:它将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理。

事务管理代码的固定模式作为一种横切关注点,可以通过 AOP 方法模块化,进而借助 Spring AOP 框架实现声明式事务管理。

Spring 在不同的事务管理 API 之上定义了一个抽象层,通过配置的方式使其生效,从而让应用程序开发人员不必了解事务管理 API 的底层实现细节,就可以使用 Spring 的事务管理机制。

Spring 既支持编程式事务管理,也支持声明式的事务管理。

Spring 提供的事务管理器

Spring 从不同的事务管理 API 中抽象出了一整套事务管理机制,让事务管理代码从特定的事务技术中独立出来。开发人员通过配置的方式进行事务管理,而不必了解其底层是如何实现的。

Spring 的核心事务管理抽象是PlatformTransactionManager。它为事务管理封装了一组独立于技术的方法。无论使用 Spring 的哪种事务管理策略 (编程式或声明式),事务管理器都是必须的。

事务管理器可以以普通的 bean 的形式声明在 Spring IOC 容器中。

事务管理器的主要实现

DataSourceTransactionManager:在应用程序中只需要处理一个数据源,而且通过 JDBC 存取。

JtaTransactionManager:在 JavaEE 应用服务器上用 JTA(Java Transaction API) 进行事务管理

HibernateTransactionManager:用 Hibernate 框架存取数据库

image

6.3、事务代码示例

引入依赖

依赖见上节

准备数据库表

account_id 为账户 id,交由数据库维护;account_name 为账户名称;account_balance 为账户余额

image

数据库初始数据:Oneby 和 Heygo 分别由 1000 大洋

image

编写实体类

创建与数据库表对应的实体类

public class Account {

    private Integer accountId;
    private String accountName;
    private Integer accountBalance;

    public Integer getAccountId() {
        return accountId;
    }

    public void setAccountId(Integer accountId) {
        this.accountId = accountId;
    }

    public String getAccountName() {
        return accountName;
    }

    public void setAccountName(String accountName) {
        this.accountName = accountName;
    }

    public Integer getAccountBalance() {
        return accountBalance;
    }

    public void setAccountBalance(Integer accountBalance) {
        this.accountBalance = accountBalance;
    }

    @Override
    public String toString() {
        return "Account{" +
                "accountId=" + accountId +
                ", accountName='" + accountName + '\'' +
                ", accountBalance=" + accountBalance +
                '}';
    }
}

编写配置文件

  1. 需要在 Spring 配置文件中引入 context 名称空间和 tx 名称空间

  2. 注意:事务管理器的名字一定要叫 transactionManager,不然会抛异常:org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'transactionManager' available: No matching TransactionManager bean found for qualifier 'transactionManager' - neither qualifier match nor bean name match!

  3. 最后记得要使用 <tx:annotation-driven transaction-manager="transactionManager"/> 启用事务注解(在需要进行事务控制的方法或类上加 @Transactional 注解)

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

    
    <context:component-scan base-package="com.oneby"/>

    
    
    <context:property-placeholder location="classpath:jdbc.properties"/>
    
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="username" value="${prop.userName}"/>
        <property name="password" value="${prop.password}"/>
        <property name="url" value="${prop.url}"/>
        <property name="driverClassName" value="${prop.driverClass}"/>
    </bean>

    
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager" >
        <property name="dataSource" ref="dataSource"/>
    </bean>

    
    <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>

编写 DAO 层

AccountDao 接口

public interface AccountDao {

    public int tranfer(String accountName, int money);

}

AccountDaoImpl 实现类:从名为 accountName 的账户中转走 money 元大洋

@Repository
public class AccountDaoImpl implements AccountDao {

    
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public int tranfer(String accountName, int money) {
        
        String sql = "update t_accounts set account_balance = account_balance - ? where account_name = ?";

        
        Object[] args = {money, accountName};

        
        int insertRows = jdbcTemplate.update(sql, args);
        return insertRows;
    }
}

编写 Service 层:没有添加事务

AccountService 类:从名为 srcAccountName 的账户转钱到名为 destAccountName 的账户,转账金额为 money 元大洋,但是在两个转账操作中间手动制造了一个异常

@Service
public class AccountService {

    @Autowired
    private AccountDao accountDao;

    public void transfer(String srcAccountName, String destAccountName, int money) {
        accountDao.tranfer(srcAccountName, money);
        int i = 10 / 0; 
        accountDao.tranfer(destAccountName, -money);
        System.out.println(srcAccountName + " 向 " + destAccountName + " 转账 " + money + " 元");
    }

}

测试代码:Heygo 给 Oneby 转账 1000 大洋

public class SpringTest {

    @Test
    public void test() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("transaction-test.xml");

        
        AccountService accountService = iocContainer.getBean(AccountService.class);

        
        accountService.transfer("Heygo", "Oneby", 100);
    }

}

程序抛出异常

image

Heygo 账户莫名其妙少了 100 块钱

image

编写 Service 层:添加声明式事务

AccountService 类:也没啥,就加了一个 @Transactional 注解,表示为 AccountService 类中的所有方法都开启事务控制

@Service
@Transactional
public class AccountService {

    @Autowired
    private AccountDao accountDao;

    public void transfer(String srcAccountName, String destAccountName, int money) {
        accountDao.tranfer(srcAccountName, money);
        int i = 10 / 0; 
        accountDao.tranfer(destAccountName, -money);
        System.out.println(srcAccountName + " 向 " + destAccountName + " 转账 " + money + " 元");
    }

}

程序抛出异常

image

Oneby 账户和 Heygo 账户余额都没有改变呢

image

6.4、事务相关参数

6.4.1、事务传播行为

事务的传播行为概述

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能已经开启了一个新事务。事务的传播行为可以由传播属性指定。Spring 定义了 7 种类传播行为

image

REQUIRED 和 REQUIRES_NEW 传播行为

测试环境

image

REQUIRED 传播行为

bookServicepurchase()方法被另一个事务方法checkout()调用时,它默认会在现有的事务内运行。这个默认的传播行为就是REQUIRED。因此在checkout()方法的开始和终止边界内只有一个事务。这个事务只在checkout()方法结束的时候被提交,结果用户一本书都买不了

image

REQUIRES_NEW 传播行为

表示该方法必须启动一个新事务,并在自己的事务内运行。如果有事务在运行,就应该先挂起它

image

Spring 中配置事务的传播行为

1、注解方式

事务传播属性可以在@Transactional注解的propagation属性中定义

2、XML 方式

通过 <tx:method>元素的 propagation 属性设置事务的传播行为

image

6.4.2、事务隔离级别

数据库事务并发问题

假设现在有两个事务:Transaction01 和 Transaction02 并发执行。

① 脏读

[1]Transaction01 将某条记录的AGE值从 20 修改为 30。

[2]Transaction02 读取了 Transaction01 更新后的值:30。

[3]Transaction01 回滚,AGE值恢复到了 20。

[4]Transaction02 读取到的 30 就是一个无效的值。

② 不可重复读

[1]Transaction01 读取了AGE值为 20。

[2]Transaction02 将AGE值修改为 30。

[3]Transaction01 再次读取AGE值为 30,和第一次读取不一致。

③ 幻读

[1]Transaction01 读取了STUDENT表中的一部分数据。

[2]Transaction02 向STUDENT表中插入了新的行。

[3]Transaction01 读取了STUDENT表时,多出了一些行。

数据库的隔离级别

数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL 标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

①读未提交READ UNCOMMITTED,允许 Transaction01 读取 Transaction02 未提交的修改。

②读已提交READ COMMITTED,要求 Transaction01 只能读取 Transaction02 已提交的修改。

③可重复读REPEATABLE READ,确保 Transaction01 可以多次从一个字段中读取到相同的值,即 Transaction01 执行期间禁止其它事务对这个字段进行更新。

④串行化SERIALIZABLE,确保 Transaction01 可以多次从一个表中读取到相同的行,在 Transaction01 执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。

各个隔离级别解决并发问题的能力见下表

脏读不可重复读幻读
READ UNCOMMITTED
READ COMMITTED
REPEATABLE READ
SERIALIZABLE

各种数据库产品对事务隔离级别的支持程度

OracleMySQL
READ UNCOMMITTED×
READ COMMITTED
REPEATABLE READ×√(默认)
SERIALIZABLE

在 Spring 中指定事务隔离级别

1、注解方式

可以在@Transactionalisolation属性中设置隔离事务的级别

2、XML 方式

通过 <tx:method>元素的 isolation 属性设置传播事务的隔离级别

image

6.4.3、事务是否回滚

事务回滚的默认触发条件

捕获到RuntimeExceptionError时回滚,而捕获到编译时异常不回滚。

在 Spring 中设置事务的回滚

1、注解方式@Transactional 注解

[1]rollbackFor属性:指定遇到时必须进行回滚的异常类型,可以为多个

[2]noRollbackFor属性:指定遇到时不回滚的异常类型,可以为多个

image

2、XML 方式

通过 <tx:method>元素的 rollback-forno-rollback-for 属性设置事务的回滚

image

6.4.4、事务超时时间

超时事务属性

超时事务属性:事务在强制回滚之前可以保持多久。这样可以防止长期运行的事务占用资源。

在 Spring 中设置事务的超时时间

1、注解方式

通过 @Transactional 注解的 timeout 属性设置事务的超时时间,单位为 s

image

2、XML 方式

通过 <tx:method>元素的 timeout 属性设置事务的超时时间,单位为 s

image

6.4.5、事务只读属性

事务的优化:事务的只读属性

由于事务可以在行和表上获得锁,因此长事务会占用资源,并对整体性能产生影响。如果一个事物只读取数据但不做修改,数据库引擎可以对这个事务进行优化。

只读事务属性:表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。

在 Spring 中设置事务的只读属性

1、注解方式

通过 @Transactional 注解的 readOnly 属性设置事务是否只读

image

2、XML 方式

通过 <tx:method>元素的 read-only 属性设置事务是否只读

image

6.5、完全注解开发

创建 Spring 配置类

  1. @Configuration 标识这是一个配置类;@ComponentScan(basePackages = "com.oneby") 配置包扫描路径;@EnableTransactionManagement 开启注解事务管理;@PropertySource(value = "classpath:jdbc.properties") 标识 properties 配置文件的路径

  2. 使用 @Value 注解读取 jdbc.properties 配置文件中的内容,并用于配置数据库连接池 DataSource

  3. 后面配置 JdbcTemplateDataSourceTransactionManager 都需要向其指定属性中注入 DataSource 对象

@Configuration
@ComponentScan(basePackages = "com.oneby")
@EnableTransactionManagement
@PropertySource(value = "classpath:jdbc.properties")
public class TransactionConfig {

    @Value("${prop.userName}")
    private String userName;

    @Value("${prop.password}")
    private String password;

    @Value("${prop.url}")
    private String url;

    @Value("${prop.driverClass}")
    private String driverClass;

    
    @Bean
    public DruidDataSource getDruidDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(userName);
        dataSource.setPassword(password);
        dataSource.setUrl(url);
        dataSource.setDriverClassName(driverClass);
        return dataSource;
    }

    
    
    @Bean
    public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }

    
    
    @Bean
    public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource) {
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        transactionManager.setDataSource(dataSource);
        return transactionManager;
    }

}

代码测试

测试代码

public class SpringTest {
    @Test
    public void testAnnotation() {
        
        ApplicationContext iocContainer =
                new AnnotationConfigApplicationContext(TransactionConfig.class);

        
        AccountService accountService = iocContainer.getBean(AccountService.class);

        
        accountService.transfer("Heygo", "Oneby", 100);
    }

}

程序执行抛出异常

image

转账操作未成功,Oneby 和 Heygo 账户中的前分文未动,证明声明式事务配置成功

image

6.6、XML 事务管理

XML 事务管理的配置文件

XML 方式配置事务就三步:① 配置切入点表达式;② 配置通知与事务参数;③ 将切入点表达式与通知联系起来

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

    
    <context:component-scan base-package="com.oneby"/>

    
    
    <context:property-placeholder location="classpath:jdbc.properties"/>
    
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="username" value="${prop.userName}"/>
        <property name="password" value="${prop.password}"/>
        <property name="url" value="${prop.url}"/>
        <property name="driverClassName" value="${prop.driverClass}"/>
    </bean>

    
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    
    <aop:config>
        <aop:pointcut id="txPointCut" expression="execution(* com.oneby.service.AccountService.*(..))"/>
    </aop:config>

    
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        
        <tx:attributes>
            
            <tx:method name="transfer" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>

    
    <aop:config>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
    </aop:config>
    
</beans>

测试代码

public class SpringTest {

    @Test
    public void test() {
        
        ApplicationContext iocContainer =
                new ClassPathXmlApplicationContext("xml-transaction-config.xml");

        
        AccountService accountService = iocContainer.getBean(AccountService.class);

        
        accountService.transfer("Heygo", "Oneby", 100);
    }

}

程序运行结果:程序运行后抛出异常,但 Oneby 和 Heygo 账户的余额一分不少,一分不多

image

0

评论区