总体概述

广义的 Spring:Spring 技术栈

广义上的 Spring 泛指以 Spring Framework 为基础的 Spring 技术栈。

经过十多年的发展,Spring 已经不再是一个单纯的应用框架,而是逐渐发展成为一个由多个不同子项目(模块)组成的成熟技术,Spring Framework 是其他子项目的基础。例如 Spring Framework、Spring MVC、SpringBoot、Spring Cloud、Spring Data、Spring Security 等,涵盖了从企业级应用开发到云计算等各方面的内容。

狭义的 Spring:Spring Framework

狭义的 Spring 特指 Spring Framework,通常我们将它称为 Spring 框架。它提供了很多功能,例如:依赖注入(Dependency Injection)、面向切面编程(AOP)、声明式事务管理(TX)等。

功能模块 功能介绍
Core Container 核心容器,在 Spring 环境下使用任何功能都必须基于 IOC 容器。
AOP&Aspects 面向切面编程
TX 声明式事务管理。
Spring MVC 提供了面向Web应用程序的集成功能。

Spring IoC容器

关于组件

常规的三层架构处理请求流程,整个项目就是由各种组件搭建而成的:

组件是映射到应用程序中所有可重用组件的Java对象,应该是可复用的功能对象。

组件:通常是指在软件开发中具有独立功能并可复用的模块,往往封装了某些特定的行为和状态。组件通常具有一些显式的接口,可以被其他组件或系统调用。

对象:是面向对象编程中的基本概念,指的是类的实例,包含状态(属性)和行为(方法)。对象不一定是一个完整的、具有独立功能的模块,通常是某个类的实例,可能只是一个简单的结构体或数据容器。

组件一定是对象,对象不一定是组件。

只要是组件,就可以完全交给Spring 框架进行管理,Spring框架替代了程序员原有的new对象和对象属性赋值动作等。我们只需要编写元数据(配置文件)告知Spring 管理哪些类组件和他们的关系即可。

Spring具体的组件管理动作包含:

  • 组件对象实例化
  • 组件属性属性赋值
  • 组件对象之间引用
  • 组件对象存活周期管理
  • ……

综上所述,Spring 充当一个组件容器,创建、管理、存储组件。

  • 降低了组件之间的耦合性:Spring IoC容器通过依赖注入机制,将组件之间的依赖关系削弱,减少了程序组件之间的耦合性,使得组件更加松散地耦合。
  • 提高了代码的可重用性和可维护性:将组件的实例化过程、依赖关系的管理等功能交给Spring IoC容器处理,使得组件代码更加模块化、可重用、更易于维护。
  • 方便了配置和管理:Spring IoC容器通过XML文件或者注解,轻松的对组件进行配置和管理,使得组件的切换、替换等操作更加的方便和快捷。
  • 交给Spring管理的对象(组件),方可享受Spring框架的其他功能(AOP,声明事务管理)等

关于容器

程序中的普通容器:

  • 数组
  • 集合:List
  • 集合:Set

程序中的复杂容器:

Servlet 容器能够管理 Servlet(init,service,destroy)、Filter、Listener 这样的组件的一生,它是一个复杂容器。

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

我们即将要学习的 Spring IoC 容器也是一个复杂容器。它们不仅要负责创建组件的对象、存储组件的对象,销毁组件,还要负责管理组件之间依赖关系,调用组件的方法让它们工作,最终在特定情况下销毁组件。

Spring IoC 容器概念

概念

IoC容器

Spring IoC 容器

负责实例化、配置和组装 bean(组件)核心容器。容器通过读取配置元数据来获取有关要实例化、配置和组装组件的指令。

IoC(Inversion of Control)控制反转

IoC 主要是针对对象的创建和调用控制而言的,也就是说,当应用程序需要使用一个对象时,不再是应用程序直接创建该对象,而是由 IoC 容器来创建和管理,即控制权由应用程序转移到 IoC 容器中,也就是“反转”了控制权。

这种方式基本上是通过依赖查找的方式来实现的,即 IoC 容器维护着构成应用程序的对象,并负责创建这些对象。

DI (Dependency Injection) 依赖注入

DI 是指在组件之间传递依赖关系的过程中,将依赖关系在容器内部进行处理,这样就不必在应用程序代码中硬编码对象之间的依赖关系,实现了对象之间的解耦合。

在 Spring 中,DI 是通过 XML 配置文件或注解的方式实现的。它提供了三种形式的依赖注入:构造函数注入、Setter 方法注入和接口注入。

容器通过读取配置元数据来获取有关要实例化、配置和组装组件的指令。配置元数据以 XML、Java 注解或 Java 代码形式表现。它允许表达组成应用程序的组件以及这些组件之间丰富的相互依赖关系。

上图显示了 Spring 容器工作原理的高级视图。应用程序类与配置元数据相结合,拥有完全配置且可执行的系统或应用程序。

具体接口和实现类

BeanFactory 接口提供了一种高级配置机制,能够管理任何类型的对象,它是SpringIoC容器标准化超接口,甚至可以说,它就是SpringIoC容器本身。

ApplicationContextBeanFactory 的子接口。它扩展了以下功能:

  • 更容易与 Spring 的 AOP 功能集成
  • 消息资源处理(用于国际化?)
  • 特定于应用程序给予此接口实现,例如Web 应用程序的 WebApplicationContext

BeanFactory 提供了配置框架和基本功能,而 ApplicationContext 添加了更多特定的功能。 ApplicationContextBeanFactory 的完整超集。

ApplicationContext容器实现类

类型名 简介
ClassPathXmlApplicationContext 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象
FileSystemXmlApplicationContext 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象
AnnotationConfigApplicationContext 通过读取Java配置类创建 IOC 容器对象
WebApplicationContext 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。

配置方式

Spring框架提供了多种配置方式:

  1. XML配置方式:是Spring框架最早的配置方式之一,通过在XML文件中定义Bean及其依赖关系、Bean的作用域等信息。
  2. 注解方式:从Spring 2.5版本开始提供支持,可以通过在Bean类上使用注解来代替XML配置文件中的配置信息。通过在Bean类上加上相应的注解(如@Component, @Service, @Autowired等),将Bean注册到Spring IoC容器中,这样Spring IoC容器就可以管理这些Bean之间的依赖关系。
  3. Java配置类方式:从Spring 3.0版本开始提供支持,通过Java类来定义Bean、Bean之间的依赖关系和配置信息,从而代替XML配置文件的方式。Java配置类是一种使用Java编写配置信息的方式,通过@Configuration、@Bean等注解来实现Bean和依赖关系的配置。

Spring IoC 应用

Spring IoC / DI 实现步骤

配置元数据(配置)

配置元数据,就是编写交给SpringIoC容器管理组件的信息,配置方式有三种。

基于 XML 的配置元数据的基本结构:

<bean id=”…” [1] class=”…” [2]>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="..." [1] class="..." [2]>
<!-- collaborators and configuration for this bean go here -->
</bean>

<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions go here -->
</beans>

Spring IoC 容器管理一个或多个组件。这些 组件是使用你提供给容器的配置元数据(例如,以 XML <bean/> 定义的形式)创建的。

标签 == 组件信息声明

  • id 属性是标识单个 Bean 定义的字符串。
  • class 属性定义 Bean 的类型并使用完全限定的类名。

实例化IoC容器

提供给 ApplicationContext 构造函数的位置路径是资源字符串地址,允许容器从各种外部资源(如本地文件系统、Java CLASSPATH 等)加载配置元数据。

我们应该选择一个合适的容器实现类,进行IoC容器的实例化工作:

1
2
3
//实例化ioc容器,读取外部配置文件,最终会在容器内进行ioc和di动作
ApplicationContext context =
new ClassPathXmlApplicationContext("services.xml", "daos.xml");

获取Bean(组件)

ApplicationContext 是一个高级工厂的接口,能够维护不同 bean 及其依赖项的注册表。通过使用方法 T getBean(String name, Class<T> requiredType) ,可以检索 bean 的实例。

允许读取 Bean 定义并访问它们,如以下示例所示:

1
2
3
4
5
6
//创建ioc容器对象,指定配置文件,ioc也开始实例组件对象
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
//获取ioc容器的组件对象
PetStoreService service = context.getBean("petStore", PetStoreService.class);
//使用组件对象
List<String> userList = service.getUsernameList();

XML方式配置Bean

组件信息声明配置

Spring IoC 容器自动管理一个或多个 bean。这些 Bean 是使用提供给容器的配置元数据创建的(例如,以 XML <bean/> 定义的形式)。

创建maven工程,导入SpringIoC相关依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<!--spring context依赖-->
<!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.6</version>
</dependency>
<!--junit5测试-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.3.1</version>
</dependency>
</dependencies>

基于无参数构造函数

当通过构造函数方法创建一个 bean(组件对象) 时,所有普通类都可以由 Spring 使用并与之兼容。也就是说,正在开发的类不需要实现任何特定的接口或以特定的方式进行编码。只需指定 Bean 类信息就足够了。但是,默认情况下,我们需要一个默认(空)构造函数。

  1. 准备组件类
1
2
3
4
5
6
7
8
9
10
package com.atguigu.ioc;

public class HappyComponent {

//默认包含无参数构造函数

public void doWork() {
System.out.println("Hello SpringFrameWork");
}
}
  1. 创建携带spring约束的xml配置文件

  2. 编写配置文件:

    文件:resources/spring-bean-01.xml

1
2
<!-- 创建bean -->
<bean id="happyComponent" class="com.atguigu.ioc.HappyComponent"/>
  • bean标签:通过配置bean标签告诉IOC容器需要创建对象的组件信息
  • id属性:bean的唯一标识,方便后期获取Bean
  • class属性:组件类的全限定符
  • 要求当前组件类必须包含无参数构造函数,一般都默认包含了。

基于静态工厂方法实例化

除了使用构造函数实例化对象,还有一类是通过工厂模式实例化对象。

  1. 准备组件类
1
2
3
4
5
6
7
8
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}

public static ClientService createInstance() {
return clientService;
}
}
  1. xml配置文件编写

    文件:resources/spring-bean-01.xml

1
2
3
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>
  • class属性:指定工厂类的全限定符!
  • factory-method: 指定静态工厂方法,
  • 该方法必须是static方法。

基于实例工厂方法实例化

  1. 准备组建类
1
2
3
4
5
6
7
8
9
public class DefaultServiceLocator {

private static ClientServiceImplclientService
= new ClientServiceImpl();

public ClientService createClientServiceInstance() {
return clientService;
}
}
  1. xml配置文件编写

    文件:resources/spring-bean-01.xml

1
2
3
4
5
6
7
8
<!-- 将工厂类进行ioc配置 -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
</bean>

<!-- 根据工厂对象的实例工厂方法进行实例化组件对象 -->
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>
  • factory-bean属性:指定当前容器中工厂Bean 的名称
  • factory-method: 指定实例工厂方法名
  • 实例方法必须是非static的

组件依赖注入配置

通过配置文件,实现IoC容器中Bean之间的引用(依赖注入DI配置)。主要基于构造函数的依赖注入和基于 Setter 的依赖注入。

基于构造函数

基于构造函数的 DI 是通过容器调用具有多个参数的构造函数来完成的,每个参数表示一个依赖项。

  1. 准备组件类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserDao {
}


public class UserService {

private UserDao userDao;

private int age;

private String name;

public UserService(int age , String name ,UserDao userDao) {
this.userDao = userDao;
this.age = age;
this.name = name;
}
}
  1. 编写配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<!-- 场景1: 多参数,可以按照相应构造函数的顺序注入数据 -->
<beans>
<bean id="userService" class="x.y.UserService">
<!-- value直接注入基本类型值 -->
<constructor-arg value="18"/>
<constructor-arg value="赵伟风"/>

<constructor-arg ref="userDao"/>
</bean>
<!-- 被引用类bean声明 -->
<bean id="userDao" class="x.y.UserDao"/>
</beans>


<!-- 场景2: 多参数,可以按照相应构造函数的名称注入数据 -->
<beans>
<bean id="userService" class="x.y.UserService">
<!-- value直接注入基本类型值 -->
<constructor-arg name="name" value="赵伟风"/>
<constructor-arg name="userDao" ref="userDao"/>
<constructor-arg name="age" value="18"/>
</bean>
<!-- 被引用类bean声明 -->
<bean id="userDao" class="x.y.UserDao"/>
</beans>

<!-- 场景2: 多参数,可以按照相应构造函数的角标注入数据
index从0开始 构造函数(0,1,2....)
-->
<beans>
<bean id="userService" class="x.y.UserService">
<!-- value直接注入基本类型值 -->
<constructor-arg index="1" value="赵伟风"/>
<constructor-arg index="2" ref="userDao"/>
<constructor-arg index="0" value="18"/>
</bean>
<!-- 被引用类bean声明 -->
<bean id="userDao" class="x.y.UserDao"/>
</beans>

  • constructor-arg标签:指定构造参数和对应的值
  • constructor-arg标签:name属性指定参数名、index属性指定参数角标、value属性指定普通属性值

基于Setter方法

开发中,更多使用的Setter方法进行注入。

  1. 准备组件类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Class MovieFinder{

}

public class SimpleMovieLister {

private MovieFinder movieFinder;

private String movieName;

public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}

public void setMovieName(String movieName){
this.movieName = movieName;
}

// business logic that actually uses the injected MovieFinder is omitted...
}

​ 2.编写配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<bean id="simpleMovieLister" class="examples.SimpleMovieLister">
<!-- setter方法,注入movieFinder对象的标识id
name = 属性名 ref = 引用bean的id值
-->
<property name="movieFinder" ref="movieFinder" />

<!-- setter方法,注入基本数据类型movieName
name = 属性名 value= 基本类型值
-->
<property name="movieName" value="消失的她"/>
</bean>

<bean id="movieFinder" class="examples.MovieFinder"/>

  • property标签: 可以给setter方法对应的属性赋值
  • property 标签: name属性代表set方法标识、ref代表引用bean的标识id、value属性代表基本属性值

生命周期方法

我们可以在组件类中定义生命周期方法,当IoC容器实例化和销毁组件对象时调用。

类似于Servlet的init/destroy方法,我们可以在周期方法完成初始化和释放资源等工作。

周期方法声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BeanOne {

//周期方法要求: 方法命名随意,但是要求方法必须是 public void 无形参列表
public void init() {
// 初始化逻辑
}
}

public class BeanTwo {

public void cleanup() {
// 释放资源逻辑
}
}

周期方法配置:

1
2
3
4
<beans>
<bean id="beanOne" class="examples.BeanOne" init-method="init" />
<bean id="beanTwo" class="examples.BeanTwo" destroy-method="cleanup" />
</beans>

作用域

bean标签声明Bean,只是将Bean的信息配置给SpringIoC容器,

在IoC容器中,这些bean标签对应的信息转成Spring内部 BeanDefinition 对象,BeanDefinition 对象内包含了定义的信息(id,class,属性等等)。BeanDefinition和类概念一样,SpringIoC容器可以可以根据BeanDefinition对象反射创建多个Bean对象实例。

具体创建多少个Bean的实例对象,由Bean的作用域Scope属性指定。

作用域可选值,一般默认singleton

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

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

取值 含义 创建对象的时机 默认值
request 请求范围内有效的实例 每次请求
session 会话范围内有效的实例 每次会话

作用域配置:

1
2
3
4
5
6
7
8
9
<!-- scope属性:取值singleton(默认值),bean在IOC容器中只有一个实例,IOC容器初始化时创建对象 -->
<!-- scope属性:取值prototype,bean在IOC容器中可以有多个实例,getBean()时创建对象 -->
<bean id="happyMachine8" scope="prototype" class="com.atguigu.ioc.HappyMachine">
<property name="machineName" value="happyMachine"/>
</bean>

<bean id="happyComponent8" scope="singleton" class="com.atguigu.ioc.HappyComponent">
<property name="componentName" value="happyComponent"/>
</bean>

FactoryBean

FactoryBean 接口是Spring IoC容器实例化逻辑的可插拔性点。

用于配置复杂的Bean对象,可以将创建过程存储在FactoryBean 的getObject方法。

FactoryBean<T> 接口提供三种方法:

  • T getObject():

    返回此工厂创建的对象的实例。该返回值会被存储到IoC容器!

  • boolean isSingleton():

    如果此 FactoryBean 返回单例,则返回 true ,否则返回 false 。此方法的默认实现返回 true

  • Class<?> getObjectType(): 返回 getObject() 方法返回的对象类型,如果事先不知道类型,则返回 null

FactoryBean使用场景:

  1. 代理类的创建
  2. 第三方框架整合
  3. 复杂对象实例化等

Factorybean应用:

  1. 准备FactoryBean实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 实现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. 配置FactoryBean实现类

1
2
3
4
5
6
<!-- FactoryBean机制 -->
<!-- 这个bean标签中class属性指定的是HappyFactoryBean,但是将来从这里获取的bean是HappyMachine对象 -->
<bean id="happyMachine7" class="com.atguigu.ioc.HappyFactoryBean">
<!-- property标签仍然可以用来通过setXxx()方法给属性赋值 -->
<property name="machineName" value="iceCreamMachine"/>
</bean>

​ 3. 测试读取FactoryBean和FactoryBean.getObject对象

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testExperiment07() {

ApplicationContext iocContainer = new ClassPathXmlApplicationContext("spring-bean-07.xml");

//注意: 直接根据声明FactoryBean的id,获取的是getObject方法返回的对象
HappyMachine happyMachine = iocContainer.getBean("happyMachine7",HappyMachine.class);
System.out.println("happyMachine = " + happyMachine);

//如果想要获取FactoryBean对象, 直接在id前添加&符号即可! &happyMachine7 这是一种固定的约束
Object bean = iocContainer.getBean("&happyMachine7");
System.out.println("bean = " + bean);
}

FactoryBean和BeanFactory区别:

**FactoryBean **是 Spring 中一种特殊的 bean,可以在 getObject() 工厂方法自定义的逻辑创建Bean。是一种能够生产其他 Bean 的 Bean。FactoryBean 在容器启动时被创建,而在实际使用时则是通过调用 getObject() 方法来得到其所生产的 Bean。因此,FactoryBean 可以自定义任何所需的初始化逻辑,生产出一些定制化的 bean。

一般情况下,整合第三方框架,都是通过定义FactoryBean实现。

BeanFactory 是 Spring 框架的基础,其作为一个顶级接口定义了容器的基本行为,例如管理 bean 的生命周期、配置文件的加载和解析、bean 的装配和依赖注入等。BeanFactory 接口提供了访问 bean 的方式,例如 getBean() 方法获取指定的 bean 实例。它可以从不同的来源(例如 Mysql 数据库、XML 文件、Java 配置类等)获取 bean 定义,并将其转换为 bean 实例。同时,BeanFactory 还包含很多子类(例如,ApplicationContext 接口)提供了额外的强大功能。

总的来说,FactoryBean 和 BeanFactory 的区别主要在于前者是用于创建 bean 的接口,它提供了更加灵活的初始化定制功能,而后者是用于管理 bean 的框架基础接口,提供了基本的容器功能和 bean 生命周期管理。

注解方式配置Bean

注解标记与扫描

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

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

Spring 提供了以下多个注解,这些注解可以直接标注在 Java 类上,将它们定义成 Spring Bean。

注解 说明
@Component 该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。
@Repository 该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Service 该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。
@Controller 该注解通常作用在控制层(如SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。

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

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

使用注解标记示例:

普通组件

1
2
3
4
@Component
public class CommonComponent {
}

Controller组件

1
2
3
4
@Controller
public class XxxController {
}

Service组件

1
2
3
4
@Service
public class XxxService {
}

Dao组件

1
2
3
4
@Repository
public class XxxDao {
}

配置文件确定扫描范围示例:

情况1:基本扫描配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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">
<!-- 配置自动扫描的包 -->
<!-- 1.包要精准,提高性能!
2.会扫描指定的包和子包内容
3.多个包可以使用,分割 例如: com.atguigu.controller,com.atguigu.service等
-->
<context:component-scan base-package="com.atguigu.components"/>

</beans>

情况2:指定排除组件

1
2
3
4
5
6
7
<context:component-scan base-package="com.atguigu.components">

<!-- context:exclude-filter标签:指定排除规则 -->
<!-- type属性:指定根据什么来进行排除,annotation取值表示根据注解来排除 -->
<!-- expression属性:指定排除规则的表达式,对于注解来说指定全类名即可 -->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

情况3:指定扫描组件

1
2
3
4
5
6
7
8
<!-- 情况四:仅扫描指定的组件 -->
<!-- 仅扫描 = 关闭默认规则 + 追加规则 -->
<!-- use-default-filters属性:取值false表示关闭默认扫描规则 -->
<context:component-scan base-package="com.atguigu.ioc.components" use-default-filters="false">

<!-- context:include-filter标签:指定在原有扫描规则的基础上追加的规则 -->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>

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

默认情况下,类名首字母小写就是 bean 的 id。例如:SoldierController 类对应的 bean 的 id 就是 soldierController。

使用value属性指定:

1
2
3
@Controller(value = "tian")
public class SoldierController {
}

引用类型自动装配

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

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

举例:给Controller装配Service

1
2
3
4
5
6
7
8
9
10
11
@Controller(value = "tian")
public class SoldierController {

@Autowired
private SoldierService soldierService;

public void getMessage() {
soldierService.getMessage();
}

}

@Autowired注解细节

首先根据所需要的组件类型到 IOC 容器中查找。

如果能够找到唯一的 bean就直接执行装配。

如果完全找不到匹配这个类型的 bean报错装配失败。

如果和所需类型匹配的 bean 不止一个,分情况讨论。

使用 @Qualifier 注解:根据 @Qualifier 注解中指定的名称作为 bean 的id进行匹配。

没有 @Qualifier 注解:根据 @Autowired 标记位置成员变量的变量名作为 bean 的 id 进行匹配。

JSR-250注解

JSR(Java Specification Requests)是Java平台标准化进程中的一种技术规范,而JSR注解是其中一部分重要的内容。JSR是Java提供的技术规范,也只是规定了注解和注解的含义,并不是直接提供特定的实现,而是提供标准,由第三方框架(Spring)和库来实现和提供对应的功能。

JSR-250主要用于在Java EE 5中定义一些支持注解。该JSR主要定义了一些用于进行对象管理的注解,包括:

  • @Resource: 标识一个需要注入的资源,是实现Java EE组件之间依赖关系的一种方式。
  • @PostConstruct: 标识一个方法作为初始化方法。
  • @PreDestroy: 标识一个方法作为销毁方法。
  • @Resource.AuthenticationType: 标识注入的资源的身份验证类型。
  • @Resource.AuthenticationType: 标识注入的资源的默认名称。

JSR-250 @Resource注解

@Resource注解也可以完成属性注入。那它和@Autowired注解有什么区别?

  • @Resource注解是JDK扩展包中的,也就是说属于JDK的一部分。所以该注解是标准注解,更加具有通用性。(JSR-250标准中制定的注解类型。JSR是Java规范提案。)
  • @Autowired注解是Spring框架自己的。
  • @Resource注解默认根据Bean名称装配,未指定name时,使用属性名作为name。通过name找不到的话会自动启动通过类型装配。
  • @Autowired注解默认根据类型装配,如果想根据名称装配,需要配合@Qualifier注解一起用。
  • @Resource注解用在属性上、setter方法上。
  • @Autowired注解用在属性上、setter方法上、构造方法上、构造方法参数上。

@Resource注解属于JDK扩展包,高于JDK11或低于JDK8需要引入以下依赖。

1
2
3
4
5
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>

@Resource使用例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller
public class XxxController {
/**
* 1. 如果没有指定name,先根据属性名查找IoC中组件xxxService
* 2. 如果没有指定name,并且属性名没有对应的组件,会根据属性类型查找
* 3. 可以指定name名称查找! @Resource(name='test') == @Autowired + @Qualifier(value='test')
*/
@Resource
private XxxService xxxService;

//@Resource(name = "指定beanName")
//private XxxService xxxService;

public void show(){
System.out.println("XxxController.show");
xxxService.show();
}
}

基本类型赋值

@Value 通常用于注入外部化属性

声明外部配置

application.properties

1
catalog.name=MovieCatalog

xml引入外部配置

1
2
<!-- 引入外部配置文件-->
<context:property-placeholder location="application.properties" />

@Value注解读取配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.atguigu.components;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
* projectName: com.atguigu.components
*
* description: 普通的组件
*/
@Component
public class CommonComponent {

/**
* 情况1: ${key} 取外部配置key对应的值!
* 情况2: ${key:defaultValue} 没有key,可以给与默认值
*/
@Value("${catalog:hahaha}")
private String name;

public String getName() {
return name;
}

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

配置类方式配置Bean

Spring 完全注解配置(Fully Annotation-based Configuration)是指通过 Java配置类 代码来配置 Spring 应用程序,使用注解来替代原本在 XML 配置文件中的配置。相对于 XML 配置,完全注解配置具有更强的类型安全性和更好的可读性。

![](/img/spring/image (7).png)

配置类和扫描注解

@Configuration指定一个类为配置类,可以添加配置注解,替代配置xml文件。

@ComponentScan(basePackages = {“a”,”b”}) 替代<context:component-scan标签实现注解扫描。

@PropertySource(“classpath:配置文件地址”) 替代 <context:property-placeholder标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

//标注当前类是配置类,替代application.xml
@Configuration
//使用注解读取外部配置,替代 <context:property-placeholder标签
@PropertySource("classpath:application.properties")
//使用@ComponentScan注解,可以配置扫描包,替代<context:component-scan标签
@ComponentScan(basePackages = {"com.atguigu.components"})
public class MyConfiguration {

}

创建IoC容器

1
2
3
// AnnotationConfigApplicationContext 根据配置类创建 IOC 容器对象
ApplicationContext iocContainerAnnotation =
new AnnotationConfigApplicationContext(MyConfiguration.class);

此外,可以使用 no-arg 构造函数实例化 AnnotationConfigApplicationContext ,然后使用 register() 方法对其进行配置。此方法在以编程方式生成 AnnotationConfigApplicationContext 时特别有用。

1
2
3
4
5
6
7
8
// AnnotationConfigApplicationContext-IOC容器对象
ApplicationContext iocContainerAnnotation =
new AnnotationConfigApplicationContext();
//外部设置配置类
iocContainerAnnotation.register(MyConfiguration.class);
//刷新后方可生效
iocContainerAnnotation.refresh();

Bean定义组件

第三方jar包的类,添加到ioc容器,无法使用@Component等相关注解,因为源码jar包内容为只读模式。

@Bean 注释用于指示方法实例化、配置和初始化要由 Spring IoC 容器管理的新对象。@Bean 注释与 元素起着相同的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//标注当前类是配置类,替代application.xml    
@Configuration
//引入jdbc.properties文件
@PropertySource({"classpath:application.properties","classpath:jdbc.properties"})
@ComponentScan(basePackages = {"com.atguigu.components"})
public class MyConfiguration {

//如果第三方类进行IoC管理,无法直接使用@Component相关注解
//解决方案: 配置类方式,可以使用方法返回值+@Bean注解
@Bean
public DataSource createDataSource(@Value("${jdbc.user}") String username,
@Value("${jdbc.password}")String password,
@Value("${jdbc.url}")String url,
@Value("${jdbc.driver}")String driverClassName){
//使用Java代码实例化
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setUrl(url);
dataSource.setDriverClassName(driverClassName);
//返回结果即可
return dataSource;
}
}

三种配置方式总结

XML方式配置总结

  1. 所有内容写到xml格式配置文件中
  2. 声明bean通过<bean标签
  3. <bean标签包含基本信息(id,class)和属性信息 <property name value / ref
  4. 引入外部的properties文件可以通过<context:property-placeholder
  5. IoC具体容器实现选择ClassPathXmlApplicationContext对象

XML+注解方式配置总结

  1. 注解负责标记IoC的类和进行属性装配
  2. xml文件依然需要,需要通过<context:component-scan标签指定注解范围
  3. 标记IoC注解:@Component,@Service,@Controller,@Repository
  4. 标记DI注解:@Autowired @Qualifier @Resource @Value
  5. IoC具体容器实现选择ClassPathXmlApplicationContext对象

完全注解方式配置总结

  1. 完全注解方式指的是去掉xml文件,使用配置类 + 注解实现
  2. xml文件替换成使用@Configuration注解标记的类
  3. 标记IoC注解:@Component,@Service,@Controller,@Repository
  4. 标记DI注解:@Autowired @Qualifier @Resource @Value
  5. <context:component-scan标签指定注解范围使用@ComponentScan(basePackages = {“com.atguigu.components”})替代
  6. <context:property-placeholder引入外部配置文件使用@PropertySource({“classpath:application.properties”,”classpath:jdbc.properties”})替代
  7. <bean 标签使用@Bean注解和方法实现
  8. IoC具体容器实现选择AnnotationConfigApplicationContext对象

Spring AOP 面向切面编程

基本概念

AOP(面向切面编程)是一种编程范式,它通过将通用的横切关注点(如日志、事务、权限控制等)与业务逻辑分离,使得代码更加清晰、简洁、易于维护。AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。

不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系。例如日志功能,日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此。

这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP技术利用一种称为”横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为”Aspect”,即切面。所谓”切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用AOP,可以在不修改原来代码的基础上添加新功能。

AOP可以应用于各种场景,以下是一些常见的AOP应用场景:

  1. 日志记录:在系统中记录日志是非常重要的,可以使用AOP来实现日志记录的功能,可以在方法执行前、执行后或异常抛出时记录日志。
  2. 事务处理:在数据库操作中使用事务可以保证数据的一致性,可以使用AOP来实现事务处理的功能,可以在方法开始前开启事务,在方法执行完毕后提交或回滚事务。
  3. 安全控制:在系统中包含某些需要安全控制的操作,如登录、修改密码、授权等,可以使用AOP来实现安全控制的功能。可以在方法执行前进行权限判断,如果用户没有权限,则抛出异常或转向到错误页面,以防止未经授权的访问。
  4. 性能监控:在系统运行过程中,有时需要对某些方法的性能进行监控,以找到系统的瓶颈并进行优化。可以使用AOP来实现性能监控的功能,可以在方法执行前记录时间戳,在方法执行完毕后计算方法执行时间并输出到日志中。
  5. 异常处理:系统中可能出现各种异常情况,如空指针异常、数据库连接异常等,可以使用AOP来实现异常处理的功能,在方法执行过程中,如果出现异常,则进行异常处理(如记录日志、发送邮件等)。
  6. 缓存控制:在系统中有些数据可以缓存起来以提高访问速度,可以使用AOP来实现缓存控制的功能,可以在方法执行前查询缓存中是否有数据,如果有则返回,否则执行方法并将方法返回值存入缓存中。
  7. 动态代理:AOP的实现方式之一是通过动态代理,可以代理某个类的所有方法,用于实现各种功能。

AOP术语名词介绍

1-横切关注点

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

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

AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

2-通知(增强)

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

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

3-连接点 joinpoint

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

指那些被拦截到的点。在 Spring 中,可以被动态代理拦截目标类的方法

4-切入点 pointcut

定位连接点的方式,或者可以理解成被选中的连接点。

是一个表达式,比如execution(* com.spring.service.impl..(..))。符合条件的每个方法都是一个具体的连接点。

5-切面 aspect

切入点和通知的结合。是一个类。

6-目标 target

被代理的目标对象。

7-代理 proxy

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

8-织入 weave

指把通知应用到目标上,生成代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。

简单实现

动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口。

cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口。

AspectJ:早期的AOP实现的框架,SpringAOP借用了AspectJ中的AOP注解。

如果目标类有接口,选择使用jdk动态代理,如果目标类没有接口,选择cglib动态代理。

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

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

实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.atguigu.proxy;


/**
* 实现计算接口,加减乘除基本实现
*/
@Component
public class CalculatorPureImpl implements Calculator {

@Override
public int add(int i, int j) {
int result = i + j;
return result;
}

@Override
public int sub(int i, int j) {
int result = i - j;
return result;
}

@Override
public int mul(int i, int j) {
int result = i * j;
return result;
}

@Override
public int div(int i, int j) {
int result = i / j;
return result;
}
}

声明切面类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.atguigu.advice;

import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
public class LogAspect {

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

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

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

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

}

开启aspectj注解支持,配置类方式

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "com.atguigu")
@EnableAspectJAutoProxy
public class MyConfig {
}

测试效果

1
2
3
4
5
6
7
8
9
10
11
12
@SpringJUnitConfig(value = {MyConfig.class})
public class AopTest {

@Autowired
private Calculator calculator;

@Test
public void testCalculator(){
calculator.add(1,1);
}
}

1
2
3
4
"C:\Program Files\Java\jdk-17\bin\java.exe" -ea -Didea.test.cyclic.buffer.size=104857.....
[AOP前置通知] 方法开始了
[AOP返回通知] 方法成功返回了
[AOP后置通知] 方法最终结束了

重用切点表达式

上面案例,是我们之前编写切点表达式的方式,发现, 所有增强方法的切点表达式相同,出现了冗余,如果需要切换也不方便统一维护。我们可以将切点提取,在增强上进行引用。

同一类内部引用

  • 提取:
1
2
3
// 切入点表达式重用
@Pointcut("execution(public int com.atguigu.aop.api.Calculator.add(int,int)))")
public void declarPointCut() {}

提取切点注解使用@Pointcut(切点表达式) , 需要添加到一个无参数无返回值方法上即可。

  • 引用:
1
2
@Before(value = "declarPointCut()")
public void printLogBeforeCoreOperation(JoinPoint joinPoint) {
  • 不同类中引用:

不同类在引用切点,只需要添加类的全限定符+方法名即可!

1
2
@Before(value = "com.atguigu.spring.aop.aspect.LogAspect.declarPointCut()")
public Object roundAdvice(ProceedingJoinPoint joinPoint) {
  • 切点统一管理
1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class AtguiguPointCut {

@Pointcut(value = "execution(public int *..Calculator.sub(int,int))")
public void atguiguGlobalPointCut(){}

@Pointcut(value = "execution(public int *..Calculator.add(int,int))")
public void atguiguSecondPointCut(){}

@Pointcut(value = "execution(* *..*Service.*(..))")
public void transactionPointCut(){}
}

建议将切点表达式统一存储到一个类中进行集中管理和维护。

环绕通知

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 使用@Around注解标明环绕通知方法
@Around(value = "com.atguigu.aop.aspect.AtguiguPointCut.transactionPointCut()")
public Object manageTransaction(

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

// 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
Object[] args = joinPoint.getArgs();

// 通过ProceedingJoinPoint对象获取目标方法的签名对象
Signature signature = joinPoint.getSignature();

// 通过签名对象获取目标方法的方法名
String methodName = signature.getName();

// 声明变量用来存储目标方法的返回值
Object targetMethodReturnValue = null;

try {

// 在目标方法执行前:开启事务(模拟)
log.debug("[AOP 环绕通知] 开启事务,方法名:" + methodName + ",参数列表:" + Arrays.asList(args));

// 过ProceedingJoinPoint对象调用目标方法
// 目标方法的返回值一定要返回给外界调用者
targetMethodReturnValue = joinPoint.proceed(args);

// 在目标方法成功返回后:提交事务(模拟)
log.debug("[AOP 环绕通知] 提交事务,方法名:" + methodName + ",方法返回值:" + targetMethodReturnValue);

}catch (Throwable e){

// 在目标方法抛异常后:回滚事务(模拟)
log.debug("[AOP 环绕通知] 回滚事务,方法名:" + methodName + ",异常:" + e.getClass().getName());

}finally {

// 在目标方法最终结束后:释放数据库连接
log.debug("[AOP 环绕通知] 释放数据库连接,方法名:" + methodName);

}

return targetMethodReturnValue;
}

切面优先级

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

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

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

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

实际意义

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

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

小结

Spring 声明式事务

声明式事务是指使用注解或 XML 配置的方式来控制事务的提交和回滚。

开发者只需要添加配置即可, 具体事务的实现由第三方框架实现,避免我们直接进行事务操作。

使用声明式事务可以将事务的控制和业务逻辑分离开来,提高代码的可读性和可维护性。

Spring事务管理器

  1. Spring声明式事务对应依赖

    • spring-tx: 包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等)
    • spring-jdbc: 包含DataSource方式事务管理器实现类DataSourceTransactionManager
    • spring-orm: 包含其他持久层框架的事务管理器实现类例如:Hibernate/Jpa等
  2. Spring声明式事务对应事务管理器接口

    我们现在要使用的事务管理器是org.springframework.jdbc.datasource.DataSourceTransactionManager,将来整合 JDBC方式、JdbcTemplate方式、Mybatis方式的事务实现。

    DataSourceTransactionManager类中的主要方法:

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

基本事务控制

数据库相关的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Configuration
@ComponenScan("com.atguigu")
@PropertySource(value = "classpath:jdbc.properties")
@EnableTransactionManagement
public class DataSourceConfig {

/**
* 实例化dataSource加入到ioc容器
* @param url
* @param driver
* @param username
* @param password
* @return
*/
@Bean
public DataSource dataSource(@Value("${atguigu.url}")String url,
@Value("${atguigu.driver}")String driver,
@Value("${atguigu.username}")String username,
@Value("${atguigu.password}")String password){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);

return dataSource;
}

/**
* 实例化JdbcTemplate对象,需要使用ioc中的DataSource
* @param dataSource
* @return
*/
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}

/**
* 装配事务管理实现对象
* @param dataSource
* @return
*/
@Bean
public TransactionManager transactionManager(DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}

}

  1. 使用声明事务注解@Transactional
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class StudentService {

@Autowired
private StudentDao studentDao;

@Transactional
public void changeInfo(){
studentDao.updateAgeById(100,1);
System.out.println("-----------");
int i = 1/0;
studentDao.updateNameById("test1",1);
}
}

  1. 测试事务效果
1
2
3
4
5
6
7
8
9
10
11
12
@SpringJUnitConfig(classes = DataSourceConfig.class)
public class TxTest {

@Autowired
private StudentService studentService;

@Test
public void testTx(){
studentService.changeInfo();
}
}

事务属性

只读

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

设置方式:

1
2
// readOnly = true把当前事务设置为只读 默认是false
@Transactional(readOnly = true)

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
@Transactional(readOnly = true)
public class EmpService {

// 为了便于核对数据库操作结果,不要修改同一条记录
@Transactional(readOnly = false)
public void updateTwice(……) {
……
}

// readOnly = true把当前事务设置为只读
// @Transactional(readOnly = true)
public String getEmpName(Integer empId) {
……
}

}

超时时间

事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题。此时这个程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。概括来说就是超时回滚,释放资源。

设置超时时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Service
public class StudentService {

@Autowired
private StudentDao studentDao;

/**
* timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间
*/
@Transactional(readOnly = false,timeout = 3)
public void changeInfo(){
studentDao.updateAgeById(100,1);
//休眠4秒,等待方法超时
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
studentDao.updateNameById("test1",1);
}
}

事务异常

  1. 默认情况

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
public class StudentService {

@Autowired
private StudentDao studentDao;

@Transactional(readOnly = false,timeout = 3)
public void changeInfo() throws FileNotFoundException {
studentDao.updateAgeById(100,1);
//主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
new FileInputStream("xxxx");
studentDao.updateNameById("test1",1);
}
}
  1. 设置回滚异常

    rollbackFor属性:指定哪些异常类才会回滚,默认是 RuntimeException and Error 异常方可回滚!

1
2
3
4
5
6
7
8
9
/**
* noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内
*/
@Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class)
public void changeInfo() throws FileNotFoundException {
studentDao.updateAgeById(100,1);
new FileInputStream("xxxx");
studentDao.updateNameById("test1",1);
}
  1. 设置不回滚的异常

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

    noRollbackFor属性:指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class StudentService {

@Autowired
private StudentDao studentDao;

/**
* noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内
*/
@Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class,noRollbackFor = FileNotFoundException.class)
public void changeInfo() throws FileNotFoundException {
studentDao.updateAgeById(100,1);
//主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内
new FileInputStream("xxxx");
studentDao.updateNameById("test1",1);
}
}

事务隔离级别

数据库事务的隔离级别是指在多个事务并发执行时,数据库系统为了保证数据一致性所遵循的规定。常见的隔离级别包括:

  1. 读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。
  2. 读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。
  3. 可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。
  4. 串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。

不同的隔离级别适用于不同的场景,需要根据实际业务需求进行选择和调整。

  1. 事务隔离级别设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.atguigu.service;

import com.atguigu.dao.StudentDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

import java.io.FileInputStream;
import java.io.FileNotFoundException;

@Service
public class StudentService {

@Autowired
private StudentDao studentDao;

/**
* isolation = 设置事务的隔离级别,mysql默认是repeatable read!
*/
@Transactional(readOnly = false,
timeout = 3,
rollbackFor = Exception.class,
noRollbackFor = FileNotFoundException.class,
isolation = Isolation.REPEATABLE_READ)
public void changeInfo() throws FileNotFoundException {
studentDao.updateAgeById(100,1);
new FileInputStream("xxxx");
studentDao.updateNameById("test1",1);
}
}

总结

核心点 掌握目标
spring框架理解 spring家族和spring framework框架
spring核心功能 ioc/di , aop , tx
spring ioc / di 组件管理、ioc容器、ioc/di , 三种配置方式
spring aop aop和aop框架和代理技术、基于注解的aop配置
spring tx 声明式和编程式事务、动态事务管理器、事务注解、属性