Spring Framework基础

1 目录

2 Spring Framework框架简介

4.1 Spring Framework框架简介

Spring Framework共计含有20多个子模块,其框架图如下图所示:

Pasted image 20241008164128.png

其主要功能有:

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

3 Spring Framework的IoC容器

正如上一章节所述,Spring Framework提供了如下的功能:

  • 自动创建、保存组件对象(即组件对象实例化)

  • 自动进行组件对象的生命周期管理

  • 自动进行组件的组装(DI依赖注入)

  • 自动进行事务管理(TX)

  • 与Spring全家桶的其他框架进行整合交互
    而Spring要求的组件称为 Spring Bean ,其是在Java Bean要求的基础上进行规定的。
    在Spring文档中, Spring Bean 被如下定义:

    In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container.
    

即:构成应用程序主干并由Spring IoC容器管理的对象称为bean

Java Bean
Spring Bean
要求 任何符合要求的类都可以称之为Java Bean。 Spring Bean是在Spring IoC容器中被实例化、管理和维护的对象
一个Bean可以是任何普通的Java对象,例如 POJO、Service、Respository、Controller等。

3.1 IoC容器与组件

通常来说Spring项目由如下三层组成:

Pasted image 20241008172108.png

通常来说:

  • 控制层通常被命名为 XxController
  • 业务逻辑层通常被命名为 XxService
  • 持久化层通常被命名为 XxMapper 或者 XxDao

每一层都由组件构成,并且这些组件必须被放入Spring容器中才能使用一些由Spring提供的特性。而各层之间存在的有依赖关系,例如控制层在接收到请求后,会逐层调用业务逻辑层、持久化层后才能响应该请求。

而Spring可以管理并负责这些组件之间的依赖并完成装配。但是依赖信息需要由程序员按照如下三种方式之一来配置:

  1. xml配置方式
  2. 注解配置方式
  3. java类配置方式
    具体的方式可见Spring Framework基础 > 3 3 IoC控制反转与DI依赖注入

3.2 Spring IoC容器接口及其实现类介绍

在Spring中, org.springframework.beans.factory 包中定义了Spring IoC容器接口 BeanFactory 。在这个接口的定义下,Spring还提供了:

类型名
简介
ClassPathXmlApplicationContext 通过读取类路径下(src下)的xml格式的配置文件创建IoC容器对象,即:
1. 配置方式为xml
2. xml文件在类路径下
时使用此接口。
FileSystemXmlApplicationContext [不常用]通过文件系统路径下(其他路径)读取xml格式的配置文件创建IoC容器对象,即:
1. 配置方式为xml
2.xml文件在系统中的其他路径
时使用此接口。
AnnotationConfigApplicationContext 通过读取Java配置类创建IoC容器对象,即:
1. 配置文件使用的是Java类
时使用此接口。
WebApplicationContext 专门为Web应用准备,基于Web环境创建IoC容器对象,
并将对象引入存入ServletContext域中,即:
1. 当前项目为Web项目
时使用此接口。

等常用接口,这些接口都是 BeanFactory 的拓展,提供了更多的特性和功能(即上述表格中"简介"的功能)。
本章节只做简单介绍,具体实现可见章节Spring IoC容器创建和使用

3.3 IoC控制反转、IoC容器,以及DI依赖注入

基本概念:

  • IoCInversion of Control控制反转
    控制反转指的是在Spring中,类的控制权不再由开发者所编写的代码所有,而是直接归Spring IoC容器所有。在IoC容器中,
  • DIDependency injection依赖注入
    在使用容器创建或使用组件时,往往会遇到依赖和参数传递的问题。而依赖注入就提供了将依赖关系在容器内部进行处理的解决方式。

而在使用IoC容器管理组件时,需要执行如下的步骤:

  1. 通过 配置文件 注解 配置类 的方式表述需要容器管理的组件,以及组件之间的依赖关系。
  2. 通过IoC容器接口实例化一个IoC容器对象(使用IoC容器接口及其实现类)。
  3. 在Java代码中获取IoC容器中的组件并使用。

仿照Spring框架中的三层组件分层,可以先假设一个如下的组件依赖情况:

客户端请求

UserController

UserService

UserMapper
参数:dbUserName, dbPassword

Database

则上图的依赖关系为上一级依赖下一级,即:

  • UserController 依赖 UserService
  • UserService 依赖 UserMapper
  • UserMapper 需要指定若干参数

而上述的依赖关系可以通过DI依赖注入完成,依赖注入有如下几种方法:

  1. 构造函数传参
  2. setter 方法传参
  3. 使用注解自动装配

在后续子章节中,均假设:

  1. UserMapper 注入到 UserService 时使用的是构造函数传参。
  2. UserService 注入到 UserController 时:
    1. 在xml方式的示例中使用的是 setter 接口。
    2. 在注解方式的实例中使用的是注解自动装配。

并且给定Java代码如下:

UserMapper.java

package indi.h13.mappers;  
  
public class UserMapper {  
	public UserMapper(String dbUserName, String dbPassword) {  
	    System.out.println("Created a UserMapper using a constructor with dbUserName " + dbUserName + ", dbPassword " + dbPassword);  
	}
}

UserService.java

package indi.h13.services;  
import indi.h13.mappers.UserMapper; 
  
public class UserService {  
	private UserMapper mapper;
    public UserService(UserMapper mapper){ 
	    this.mapper = mapper;
    }  
}

UserController.java

package indi.h13.controllers;  
import indi.h13.services.UserService;  
  
public class UserController {  
    private UserService service;  
    public UserController() {}  
}

3.3.1 IoC容器中组件的实例化

3.3.1.1 使用xml完成IoC容器中组件的实例化

在工程中引入Spring相关的组件后,非社区板的IDEA就可以直接在 resource 目录下创建IoC组件的配置模板:

idea64_CfddUw9WFr.png

若没有该选项,则可以直接使用如下的基本模板:

<?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">  

</beans>

xml文件放置于resources文件夹后,经过编译后均会出现在 target/classes 文件夹下。
xml文件名可以随意命名,因为还需要调用对应的接口指定xml路径才能让Spring完成IoC组件实例化。具体见章节Spring IoC容器创建和使用

随后在 beans 块中完成各bean的实例化配置即可。
实例化配置有如下几种方法:

  1. 使用无参构造函数实例化
  2. 使用静态工厂类(静态方法)实例化
  3. 基于实例工厂方法(非静态方法)实例化

对应的实例化demo如下:

<?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">  
  
    <!--  
        1. 使用无参构造函数实例化组件  
            等效伪代码为:  
                UserMapper userMapper1 = UserMapper();  
            id为组件标识,要求唯一  
            class为组价的全限定符  
    -->  
    <bean id="userMapper1" class="indi.h13.mappers.UserMapper"/>  
  
    <!--  
        一个类可以被实例化为多个不同id的组件  
    -->  
    <bean id="userMapper2" class="indi.h13.mappers.UserMapper"/>  
  
    <!--  
        2. 使用静态工厂类(静态方法)实例化组件  
            等效伪代码为:  
                UserService userService1 = UserService.createUserService();  
            id为组件标识,要求唯一  
            class为组价的全限定符  
            factory-method为静态实例化方法  
    -->  
    <bean id="userService1" class="indi.h13.services.UserService" factory-method="createUserService"/>  
  
    <!--  
        3. 基于实例工厂方法(非静态方法)实例化  
            需要先创建一个能生成该类的对象,随后用该对象生成目标对象。            等效伪代码为:  
                ServiceLocator serviceLocator = new ServiceLocator();                UserService userService2 = serviceLocator.createUserService();  
            id为Bean标识,要求唯一  
            factory-bean为生成该方法的Bean的id  
            factory-method为实例化该类的方法  
    -->  
    <bean id="serviceLocator" class="indi.h13.ServiceLocator" />  
    <bean id="userService2" factory-bean="serviceLocator" factory-method="createUserService" />  
  
</beans>
3.3.1.2 使用注解完成IoC容器中组件的实例化

使用注解完成IoC容器中组件的实例化步骤有:

  1. 在类上添加IoC注解,IoC为组件提供的注解可见下方列表。
  2. 向Spring IoC中容器添加需要扫描含有IoC注解的Package(即配置注解生效包)

IoC为组件提供的注解有:

注解
含义及用途
@Component 该注解用于描述Spring中的组件。
Spring中的任意组件均可使用该注解,包括但不限于Spring三层架构中的任意一层。
在非三层架构的组件开发中常用。
该注解用于标注于类上。
@Repository 功能与 @Component 没有任何区别。
但是在程序可读性上用于向程序员表示该组件属于数据访问层。
@Service 功能与 @Component 没有任何区别。
但是在程序可读性上用于向程序员表示该组件属于业务逻辑层。
@Controller 功能与 @Component 没有任何区别。
但是在程序可读性上用于向程序员表示该组件属于控制层。

因此,上述三个实例Java类用注解实例化的示例为:

UserController.java

package indi.h13.controllers;  
import indi.h13.services.UserService;  

@Controller
public class UserController {  
    private UserService service;  
    public UserController() {}  
}

注:

  • 上述情况下的 @Controller 注解等效于xml配置 <bean id="userController" class="indi.h13.controllers.UserController />"
  • 默认Bean id为类首字母小写的写法,即 userController修改方式可见下例

UserMapper.java

package indi.h13.mappers;  

@Repository(value="userMapper01")
public class UserMapper {  
	public UserMapper(String dbUserName, String dbPassword) {  
	    System.out.println("Created a UserMapper using a constructor with dbUserName " + dbUserName + ", dbPassword " + dbPassword);  
	}
}

注:

  • 注解中的value属性用于修改Bean id,修改为 userMapper01
  • 单参数时可以省略为 @Repository("userMapper01")

UserService.java

package indi.h13.services;  
import indi.h13.mappers.UserMapper; 

@Service
public class UserService {  
	private UserMapper mapper;
    public UserService(UserMapper mapper){ 
	    this.mapper = mapper;
    }  
}

随后执行第二步,配置注解生效包。配置注解生效包需要在xml文件中进行。
和上一小节中使用xml完成实例化相同,通常在 resources 文件夹下创建一个Spring Config模板文件,并向其中添加若干可选配置语句即可。
可选的配置语句有:

  1. 添加注解生效包
  2. 添加指定包下除了指定注解以外的所有注解
  3. 只添加指定包下的指定注解
    Demo如下:
<?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">  
  
    <!-- 以下Demo为独立Demo,根据需求选用一个即可 -->  
    <!-- Demo 1: 扫描包indi.h13下所有关于组件的注解 -->  
    <context:component-scan base-package="indi.h13"/>  
  
    <!-- Demo 2: 扫描包indi.h13下除了Service以外的注解 -->  
    <context:component-scan base-package="indi.h13">  
        <!-- 屏蔽Service注解 -->  
        <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>  
    </context:component-scan>  
  
    <!-- Demo 3: 扫描包indi.h13下所有Controller注解(只要Controller) -->  
    <!-- 注意多了个 use-default-filters 属性!!! -->  
    <context:component-scan base-package="indi.h13" use-default-filters="false">  
        <!-- 只扫描(保留)Controller注解 -->  
        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>  
    </context:component-scan>  
  
</beans>

3.3.2 IoC容器中的DI依赖注入

3.3.2.1 使用xml完成DI依赖注入

在章节使用IoC完成容器中组件的实例化中给出了实例化的若干方法。在这些方法的基础之上完成了如下的两种DI依赖注入方式及其若干实现方法:

<?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="userMapper" class="indi.h13.mappers.UserMapper">  
        <!--  
            构造函数注入,方式一:直接顺序填写参数  
            value为直接引用值。不管实际使用的是什么类型,在xml中均要配置为字符串。  
            ref为引用其他的spring bean。  
        -->  
        <constructor-arg value="root"/> <!-- dbUserName = "root" -->  
        <constructor-arg value="pswd"/> <!-- dbPassword = "pswd" -->  
    </bean>  
  
    <bean id="userService" class="indi.h13.services.UserService">  
        <!--  
            构造函数注入,方式二[推荐]:指定参数名称并给定值  
            value为直接引用值。不管实际使用的是什么类型,在xml中均要配置为字符串。  
            ref为引用其他的spring bean。  
        -->  
        <constructor-arg name="mapper" ref="userMapper"/>  
    </bean>  

    <bean id="userMapper2" class="indi.h13.mappers.UserMapper">  
        <!--  
            构造函数注入,方式三:指定参数角标并赋值
        -->  
        <constructor-arg index="0" value="root"/> <!-- dbUserName = "root" -->  
        <constructor-arg index="1" value="pswd"/> <!-- dbPassword = "pswd" --> 
    </bean>  

    <bean id="userController" class="indi.h13.controllers.UserController">  
        <!--  
	        setter接口注入[重要]:使用setter接口注入。此时使用property标签  
            name为setter实际操作的私有变量的名称  
        -->  
        <property name="service" ref="userService"/>  
    </bean>
</beans>

需要注意的是,上述例子中的最后一种使用setter接口的方法需要实现setter方法:

UserController.java

package indi.h13.controllers;  
import indi.h13.services.UserService;  
import lombok.Setter;  

@Setter
public class UserController {  
    private UserService service;  
    public UserController() {}  
}
3.3.2.2 使用注解完成依赖注入

与xml配置中一样,使用注解的依赖注入也可以分为引用类型装配和值类型装配。
值类型的装配使用注解:

  • @Value
    使用注解完成依赖注入时,可以使用:
  • @Autowired 注解
  • @Qualifier 注解
  • @Resource 注解
    其各种特性如后续子章节所述。
3.3.2.2.1 使用Value进行值类型装配

@Autowired 注解的特性有:

  • 该注解可以被加到:
    • 类的一个字段上。
    • 类的某个方法上
    • 方法的形参上。可直接完成setter方法的依赖注入
    • 注解上

在父章节中的例子中,UserMapper.java 需要指定数据库用户名和密码,在使用注解完成依赖注入时可以按照如下的方式处理:

方式一,直接给定值(不常用,和直接代码里面写死没太大差异):

package indi.h13.mappers;  

@Repository(value="userMapper01")
public class UserMapper {  
	@Value("root")
	private String dbUserName;
	@Value("pswd")
	private String dbPassword;

	public UserMapper() {  
	    System.out.println("Created a UserMapper using a constructor with dbUserName " + this.dbUserName + ", dbPassword " + this.dbPassword);  
	}
}

方式二,引用配置文件(推荐)

配置文件可以存放于 resources 文件夹下,在SpringConfig的xml配置文件中添加 context:property-placeholder 块即可完成配置,示例如下:

<?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">  

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

上述示例中, jdbc.properties 为配置文件名,且通常使用此名称。
随后可以在上述文件中写入如下配置,即可使用 Value 注解读取并注入依赖:

jdbc.username=root
jdbc.password=pswd

随后使用如下的注解即可完成注入:

package indi.h13.mappers;  

@Repository(value="userMapper01")
public class UserMapper {  
	@Value("${jdbc.username:admin}")
	private String dbUserName;
	@Value("${jdbc.password}")
	private String dbPassword;

	public UserMapper() {  
	    System.out.println("Created a UserMapper using a constructor with dbUserName " + this.dbUserName + ", dbPassword " + this.dbPassword);  
	}
}

注:

  • @Value("${jdbc.username:admin}") 表示若配置文件中不含 jdbc.username 项,则填入默认值 admin
3.3.2.2.2 使用Autowired进行引用类型装配

@Autowired 注解的特性有:

  • 该注解可以被加到:
    • 构造函数上。可直接完成构造函数传参方式的依赖注入
    • 类的某个方法上
    • 方法的形参上。可直接完成setter方法的依赖注入
    • 类的一个字段上。
    • 注解上
  • 可以配置是否是佛系装配。即找不到满足要求的组件时跳过装配。不过该方法不常用也不推荐使用。
  • 注解实现依赖注入的工作流程

Bean唯一

Bean不唯一

没有该类型的Bean

找到对应的Bean

找不到对应的Bean

强制装配

佛系装配

运行时启动装配

检测需要注入的依赖类型

判定属于该类型的
bean是否唯一

执行装配

尝试根据Bean ID查找

检查是否强制装配
详见下

尝试使用BeanID搜索
搜索时使用注解标注的变量/参数名

抛出运行时错误

跳过装配

  • 使用该注解不需要提供setter方法,并且在实际工程中应当使用此方法

使用Demo为:

UserService.java

package indi.h13.services;  
import indi.h13.mappers.UserMapper; 

@Service
public class UserService {  
	private UserMapper mapper;
	@Autowired
    public UserService(UserMapper userMapper){ 
	    this.mapper = mapper;
    }  
}

注:

  • 上述例子直接根据 UserMapper 类型寻找组件并装配

UserController.java

package indi.h13.controllers;  
import indi.h13.services.UserService;  

@Controller
public class UserController {  
	@Autowired(required=false)
    private UserService service;  
    public UserController() {}  
}

注:

  • 上述例子中设置了 required=false找不到满足要求的组件时可以跳过
  • 如果不指定该属性,则默认为强制装配
3.3.2.2.3 使用Qualifier指定BeanID进行引用类型装配

@Qualifier 注解的特性有:

  • 直接使用BeanID进行查找装配
    本注解可以用于多实例的容器,在多实例时可以指定Bean ID来进行装配。不过不常用。

假设组件 UserMapper 的Bean ID为 userMapper1userMapper2 ,则可以通过使用 @Qualifier 指定ID的方式来进行指定装配。

UserService.java

package indi.h13.services;  
import indi.h13.mappers.UserMapper; 

@Service
public class UserService {  
	private UserMapper mapper;
	@Qualifier(value="userMapper1")
    public UserService(UserMapper userMapper){ 
	    this.mapper = mapper;
    }  
}

注:

  • @Autowired 在多例模式下,也可以通过搜索属性/参数名的方式搜索对应的BeanID,即下方代码也可以实现上述效果:
    ```Java
    package indi.h13.services;
    import indi.h13.mappers.UserMapper;

@Service
public class UserService {
private UserMapper mapper;
// 下方注解会先搜索UserMapper类的组件
// 若找到多例,则根据userMapper1这个参数名来搜索BeanID
@Autowired
public UserService(UserMapper userMapper1){
this.mapper = mapper;
}
}
```

3.3.2.2.4 使用Resource注解先匹配类型后尝试ID进行引用类型装配

@Resource 注解是由Java定义的一种规范,由Spring实现的注解。该注解的功能是先使用Autowire进行尝试装配,若失败后再尝试使用Qualifier装配的一种注解。
功能逻辑如下:

匹配成功

匹配失败

匹配成功

匹配失败

@Resource(name=xx)

尝试使用@Autowired装配

装配成功

尝试使用@Qualifier(name=xx)装配

装配失败、运行时错误

3.3.3 引入第三方Bean组件

引入第三方Bean组件也是开发中常用的一个需求或实现方式,而第三方的Bean组件在装配的时候也需要指定Bean ID,并且往往也需要DI依赖注入。
这里以引入druid连接池 com.alibaba.druid 为例,完成以下内容:

  1. com.alibaba.druid.pool.DruidDataSource 实例化为Bean ID为 dataSource 的组件。
  2. 将:
    - url:存放于 jdbc.properties 中的 jdbc.url 中。
    - driverClassName:存放于 jdbc.properties 中的 jdbc.driver 中。
    - username:存放于 jdbc.properties 中的 jdbc.username 中。
    - password:存放于 jdbc.properties 中的 jdbc.password 中。
    注入组件
    则xml配置和注解配置方式分别如子章节所述。
3.3.3.1 使用xml引入第三方Bean组件

在SpringConfig中完成如下内容:

<?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">  

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

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">  
	    <property name="url" value="{jdbc.url}" />
	    <property name="driverClassName" value="{jdbc.driver}" />
	    <property name="username" value="{jdbc.username}" />
	    <property name="password" value="{jdbc.password}" />
    </bean>  
    
</beans>

即可实现引入第三方组件。

3.3.3.2 使用注解引入第三方Bean组件
本章节的方法涉及注解配置类,详见:
3.3.4.2 使用注解配置类引入第三方Bean
3.3.4.2 使用注解配置类引入第三方Bean

本章节中注解配置类的基本假设同引入第三方Bean组件
为满足上述要求,则应在上一子章节的注解配置类中添加如下代码和 @Bean 注解:

1
// 配置包扫描注解,`value = ` 可以省略
2
@ComponentScan(value = { "indi.h13.package1", "indi.h13.package2", ...})
3
// 配置配置文件名注解,`value = ` 可以省略
4
@PropertySource(value = "classpath:jdbc.properties")
5
@Configuration
6
public class JavaConfiguration {
7

8
@Value("{jdbc.url}")
9
private String url;
10
@Value("{jdbc.driver}")
11
private String driver;
12
@Value("{jdbc.username}")
13
private String username;
14
@Value("{jdbc.password}")
15
private String password;
16
17
@Bean
18
public DruidDataSource dataSource() {
19
DruidDataSource dataSource = new DruidDataSource();
20
dataSource.setUrl(this.url);
21
dataSource.setDriverClassName(this.driver);
22
dataSource.setUsername(this.username);
23
dataSource.setPassword(this.password);
24
return dataSource;
25
}
26
}

在上述示例中:

  • @Bean生成的组件ID与方法名相同
  • @Bean生成的组件类型与方法的返回类型相同
  • @Bean注解会自动生成组件并组装
3.3.4.2.1 @Bean详解

@Bean是Spring中的一种用于直接生成Bean组件的注解,其最标准的使用方式是定义于@Configuration所注解的配置类中。其拥有如下的配置参数:

  • name & value:指定Bean ID。缺省时生成的组件ID与方法名相同。
  • initMethod & destroyMethod:指定初始化和销毁方法。默认时调用Bean类型的初始化和销毁方法。
  • autowireCandidate:是否允许该组件被其他组件引用或修改。
    @Bean生成的组件也可以使用后续章节中的注解修改组件作用域的方法。

3.3.4 使用配置类代替注解开发中的xml操作

在上述使用注解进行组件实例化和依赖注入时,都需要对xml进行配置,指定组件扫描和设置外部配置文件。而配置类就是用于替代xml操作的一种方法。
通常的操作是创建一个 config 包,编写 JavaConfiguration 类(类名包名随意,在IoC容器创建时引用即可),具体操作为:

  1. JavaConfiguration 类添加 @Configuration 注解、
  2. 配置包扫描配置注解( @ComponentScan )
  3. 配置外部配置文件注解( @PropertySource )
  4. 声明依赖的第三方Bean组件
3.3.4.1 使用注解配置类完成包扫描配置和外部配置文件配置

Demo如下:

// 配置包扫描注解,`value = ` 可以省略
@ComponentScan(value = { "indi.h13.package1", "indi.h13.package2", ...})
// 配置配置文件名注解,`value = ` 可以省略
@PropertySource(value = "classpath:jdbc.properties")
@Configuration
public class JavaConfiguration {
}

此时的配置类与如下的xml等效:

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

    <context:component-scan base-package="indi.h13.package1"/>  
    <context:component-scan base-package="indi.h13.package2"/>  
	<context:property-placeholder location="classpath:jdbc.properties"/>
	
</beans>
3.3.4.2 使用注解配置类引入第三方Bean

本章节中注解配置类的基本假设同引入第三方Bean组件
为满足上述要求,则应在上一子章节的注解配置类中添加如下代码和 @Bean 注解:

// 配置包扫描注解,`value = ` 可以省略
@ComponentScan(value = { "indi.h13.package1", "indi.h13.package2", ...})
// 配置配置文件名注解,`value = ` 可以省略
@PropertySource(value = "classpath:jdbc.properties")
@Configuration
public class JavaConfiguration {

	@Value("{jdbc.url}")
	private String url;
	@Value("{jdbc.driver}")
	private String driver;
	@Value("{jdbc.username}")
	private String username;
	@Value("{jdbc.password}")
	private String password;
	
	@Bean
	public DruidDataSource dataSource() {
		DruidDataSource dataSource = new DruidDataSource();
		dataSource.setUrl(this.url);
		dataSource.setDriverClassName(this.driver);
		dataSource.setUsername(this.username);
		dataSource.setPassword(this.password);
		return dataSource;
	}
}

在上述示例中:

  • @Bean生成的组件ID与方法名相同
  • @Bean生成的组件类型与方法的返回类型相同
  • @Bean注解会自动生成组件并组装
3.3.4.2.1 @Bean详解

@Bean是Spring中的一种用于直接生成Bean组件的注解,其最标准的使用方式是定义于@Configuration所注解的配置类中。其拥有如下的配置参数:

  • name & value:指定Bean ID。缺省时生成的组件ID与方法名相同。
  • initMethod & destroyMethod:指定初始化和销毁方法。默认时调用Bean类型的初始化和销毁方法。
  • autowireCandidate:是否允许该组件被其他组件引用或修改。
    @Bean生成的组件也可以使用后续章节中的注解修改组件作用域的方法。
3.3.4.3 使用Import注解合并多个配置类

@Import注解的作用就是将一个配置类导入到另一个配置类中,其效果等效于将多个配置类合并为一个配置类。

Demo:

@ComponentScan(value = { "indi.h13.package1", "indi.h13.package2", ...})
@PropertySource(value = "classpath:jdbc.properties")
@Configuration
public class JavaConfiguration1 {
}
@ComponentScan(value = { "indi.h13.package3", "indi.h13.package4", ...})
@PropertySource(value = "classpath:jdbc.properties")
@Configuration
public class JavaConfiguration2 {
}

随后可以使用@Import注解将 JavaConfiguration2 合并进 JavaConfiguration1

@Import(JavaConfiguration2.class)
@ComponentScan(value = { "indi.h13.package1", "indi.h13.package2", ...})
@PropertySource(value = "classpath:jdbc.properties")
@Configuration
public class JavaConfiguration1 {
}

然后在创建IoC容器时指定 JavaConfiguration1 即可创建容器。创建IoC容器的具体操作见后续章节。

3.3.5 组件作用域

Spring的组件作用域是指在IoC容器中被创建、存活以及被访问的规则
Spring组件作用域主要有如下几种:

  1. Singleton,单例模式
    • 本模式为Spring的默认作用域
    • 在单例模式下,一个IoC容器中一个组件只会有一个实例
  2. Prototype,原型模式
    • 每次:
    • 时都会创建一个新的实例
  3. Request,请求模式
  4. Session,会话模式
  5. Application,应用模式
  6. Websocket,Websocket会话模式
    通常来说使用的都是单例模式。

3.4 Spring IoC容器创建和使用

上述章节介绍了在IoC中注册组件和管理组件的方式,本章节将具体讲解IoC容器的创建和使用。

3.4.1 创建IoC容器

如章节3.2所述,Spring IoC容器提供了如下的实现类:

3.2 Spring IoC容器接口及其实现类介绍

3.2 Spring IoC容器接口及其实现类介绍

在Spring中, org.springframework.beans.factory 包中定义了Spring IoC容器接口 BeanFactory 。在这个接口的定义下,Spring还提供了:

类型名
简介
ClassPathXmlApplicationContext 通过读取类路径下(src下)的xml格式的配置文件创建IoC容器对象,即:
1. 配置方式为xml
2. xml文件在类路径下
时使用此接口。
FileSystemXmlApplicationContext [不常用]通过文件系统路径下(其他路径)读取xml格式的配置文件创建IoC容器对象,即:
1. 配置方式为xml
2.xml文件在系统中的其他路径
时使用此接口。
AnnotationConfigApplicationContext 通过读取Java配置类创建IoC容器对象,即:
1. 配置文件使用的是Java类
时使用此接口。
WebApplicationContext 专门为Web应用准备,基于Web环境创建IoC容器对象,
并将对象引入存入ServletContext域中,即:
1. 当前项目为Web项目
时使用此接口。

等常用接口,这些接口都是 BeanFactory 的拓展,提供了更多的特性和功能(即上述表格中"简介"的功能)。
本章节只做简单介绍,具体实现可见章节Spring IoC容器创建和使用

在本章主要给出如子章节所述的几种实例化容器的方法。

3.4.1.1 读取xml配置文件实例化IoC容器

下方提供了两种读取xml配置文件实例化IoC容器的方法:

  1. 直接指定类路径下的xml文件名实例化
  2. 先创建容器,随后指定配置文件并刷新
    其中方法1更常用,但是方法2也需要了解,因为在Spring框架中使用的是本方法
// 1. [重要]直接指定类路径下的xml文件名实例化
ApplicationContext context = new ClassPathXmlApplicationContext("spring-01.xml");

// 2. [了解]先创建容器,随后指定配置文件并刷新
// 在Spring框架中使用的是本方法
ApplicationContext context = new ClassPathXmlApplicationContext();
context.setConfigLocations("spring-01.xml");
context.refresh();
3.4.1.2 [推荐]使用配置类实例化IoC容器

下方也提供了两种使用配置类实例化IoC容器的方法,重点是第一种。

// 1. [重要]使用配置类实例化容器
ApplicationContext context = new AnnotationConfigApplicationContext(JavaConfiguration.class);

// 2. [了解]先创建容器,随后指定配置类并刷新
ApplicationContext context = new AnnotationConfigApplicationContext();
context.register(JavaConfiguration.class);
context.refresh();

3.4.2 操作IoC容器及组件

3.4.2.1 获取容器中的组件
// 方式1[不推荐]:使用 `beanId` 获取,返回值类型为Object,需要使用强制类型转换操作对象。  
Object userMapperBean = context.getBean("userMapper");  
UserMapper userMapper = (UserMapper)userMapperBean;  
  
// 方式2[推荐]:使用 `beanId` 获取,同时指定类型  
// 此时不需要强转  
userMapper = context.getBean("userMapper", UserMapper.class);  
  
// 方式3:只使用类型获取,此时注意:  
// 1. 此方法只支持单例,即同一个类型在容器中只有一个Bean。否则运行时出错(NoUniqueBeanDefinitionException)  
// 2. 当使用父类或接口作为类型去获取时,也可以正常获取(只要 bean instanceof A.class == true即可,且整个项目中该父类只有一个实例)  
userMapper = context.getBean(UserMapper.class);
3.4.2.2 组件周期方法

在ioc容器中,"组件周期方法"的"周期"特指"声明周期",而非"定时或重复发生的周期"。
而Spring Bean的生命周期主要有如下几个阶段:

  1. 实例化:创建Bean的实例。
  2. 填充属性:根据配置文件或注解,注入 Bean 的属性。
  3. Bean ID的赋予:如果Bean实现了 BeanNameAware 接口,Spring 容器将调用 setBeanName 方法传递Bean ID。
  4. Bean Factory的赋予:如果Bean实现了 BeanFactoryAwareApplicationContextAware 接口,Spring容器将调用 setBeanFactorysetApplicationContext 方法传递当前的应用上下文。
  5. 前置处理:Bean的前置处理器(BeanPostProcessor 的实现)的 postProcessBeforeInitialization 方法被调用。
  6. 初始化:如果Bean实现了 InitializingBean 接口,调用 afterPropertiesSet 方法。另外,如果Bean的定义包含 init-method,该方法也会被调用。
  7. 后置处理:Bean的后置处理器(BeanPostProcessor 的实现)的 postProcessAfterInitialization 方法被调用。
  8. 使用:在经历上述阶段后,此时Bean可以正常工作了,直到容器关闭。
  9. 销毁:如果Bean实现了 DisposableBean 接口,调用 destroy 方法。如果 Bean 的定义包含 destroy-method,该方法也会被调用。

IoC容器为上述生命周期的几乎所有阶段中提供了配置组件周期方法的回调,也有许多种实现或注册的方式,具体见各子章节。

3.4.2.2.1 xml配置组件周期声明周期方法的回调

使用SpringConfig可以配置如下两个生命周期方法的回调:

  • 初始化方法
  • 销毁方法
    不过此方法过于复杂,仅作为了解。

配置初始化方法的Demo:

  1. 在类中定义一个初始化方法,命名随意:
package indi.h13.controllers;  
import indi.h13.services.UserService;  

public class UserController {  
    public UserController() {}  
    public void init() {  
        System.out.println("UserController inited.");  
    }  
}
  1. 在xml中注册Bean时添加 init-method 属性,并指向该初始化方法。
<bean id="userController" class="indi.h13.controllers.UserController" init-method="init" />

此外,添加 destroy-method 属性也可以注册销毁方法。

3.4.2.2.2 通过实现接口来配置生命周期方法的回调

在上述生命周期的若干声明周期中,分别可以通过如下方法完成方法回调的配置:

  1. Bean ID赋予时回调:
    1. 实现 BeanNameAware 接口
    2. setBeanName 方法接收Bean ID
  2. Bean Factory的赋予时回调:
    1. 实现 BeanFactoryAwareApplicationContextAware 接口
    2. setBeanFactorysetApplicationContext 方法接收当前的应用上下文
  3. 前置处理时回调:
    1. 实现 BeanPostProcessor 接口
    2. 通过 postProcessBeforeInitialization 方法接收回调
  4. 初始化时回调
    1. 实现 InitializingBean 接口
    2. 通过 afterPropertiesSet 接收回调
  5. 后置处理时回调:
    1. 实现 BeanPostProcessor 接口
    2. 通过 postProcessAfterInitialization 方法接收回调
  6. 销毁时回调
    1. Bean实现 DisposableBean 接口
    2. 通过 destroy 接收回调

Demo如下:

package indi.h13.controllers;  
import indi.h13.services.UserService;  
import org.springframework.beans.factory.DisposableBean;  
import org.springframework.beans.factory.InitializingBean;  
  
public class UserController implements InitializingBean, DisposableBean {  
    private UserService service;  
    public UserController() {}  
  
    @Override  
    public void afterPropertiesSet() throws Exception {  
        System.out.println("UserController inited.");  
    }  
  
    @Override  
    public void destroy() throws Exception {  
        System.out.println("UserController destoried.");  
    }  
}

3.5 使用工厂模式FactoryBean封装复杂Bean

考虑一个场景,现在工程需要一个负责对接数据库读写的组件,该组件的实例化需要如下步骤:

  1. 创建一个SQL Session的构造工具的构造器( new SqlSessionFactoryBuilder )
  2. 构造用于读取配置文件的输入流( stream = getResourceAsStream("xx") )
  3. 使用步骤1中的构造工具的构造器来构造构造工具SQL Session( SqlSessionFactoryBuilder.builder(stream) )
  4. 使用构造工具构造SQL Session(SqlSessionFactory.openSession())
    在上述过程中共计创建了4个对象,且均不满足Spring Bean要求。
    上述需求也可以使用普通Spring Bean的组件周期方法重写init和destroy方法完成初始化和销毁流程,但是当逻辑足够复杂时,通常需要额外创建一个辅助构造类来构造这个对象,从而隐藏逻辑,简化应用操作。本章节的FactoryBean接口即用于实现该需求。

上述的例子 SqlSession 相对复杂,因此其套了两层构造来实现。假设现在有一个新的目标对象 WebSession ,则可以考虑实现一个 WebSessionFactory 的构造器工具来构造这个 WebSession 对象。

注:

  • 注意区分 FactoryBeanBeanFactory

4 AOP面相切面编程

面向切面编程(Aspect-Oriented Programming,简称 AOP),正如其名所述,其主要功能是通过代理劫持某个函数调用或者方法调用,并在这个方法调用之前或者之后去增加一个切面,并在这个切面中加入自己的业务逻辑

几个常见的业务场景:

  1. 在执行用户请求之前执行token校验
  2. 批量记录某些函数或者方法被传入的参数,以及这些方法处理的结果(即日志)

4.1 静态代理与动态代理的基本概念

如上所述,面向切面编程主要是通过代理劫持某个函数调用,并为其增加和劫持若干切面,完成切面处业务逻辑的一种程序设计模式。
这种代理通常有两种实现方式:

  1. 静态代理
  2. 动态代理

4.1.1 静态代理

静态代理就是指直接通过实现代理类的方式完成添加切面与增加切面功能的方式。这种方式的代理类在编译前就已经被明确定义,且通常需要程序员手动完成。因此在这里静态代理仅会给出一个静态代理的demo并思考其所存在的问题。

// 定义服务接口
public interface IService {
    void serve();
}

// 实现服务接口的具体类
public class Service implements IService {
    @Override
    public void serve() {
        System.out.println("Serving...");
    }
}

// 代理类,也实现IService接口
public class ServiceProxy implements IService {
    private IService service; // 内部持有一个IService的引用,通常是目标对象

    public ServiceProxy(IService service) {
        this.service = service;
    }

    @Override
    public void serve() {
        System.out.println("Before serving"); // 添加额外的逻辑
        service.serve(); // 调用原服务方法
        System.out.println("After serving"); // 添加额外的逻辑
    }
}

public class Controller {
	@Autowired
	// ServiceProxy和Service本质并不是同一个类,因此需要使用接口类型接值
	private IService serviceProxy;

	@GetMapping("/controller")
	public void controller(HttpServletRequest request, HttpServletResponse response) {
		serviceProxy.serve();
	}
}

在上述静态代理的Demo中,其本质就是定义一个代理类,并在代理类中的同名方法劫持并封装为新的同名代理方法,并在后续程序中使用代理类完成面向切面编程。
但是上述代码存在的问题是代理类和代理方法需要手动生成,并且较为麻烦。而动态代理解决了这一问题。

4.1.2 动态代理

动态代理就是不需要程序员手动实现、代理类在编译前未被定义、在编译中自动生成代理类的编程方式。

常用的动态代理实现方式有两种:

  1. JDK动态代理:本方法由JDK原生实现,其特性和要求为:
    1. 被代理的目标类必须实现一个接口
    2. 代理对象会和目标对象有同样的接口,但是并不是同一个类(和上述的静态代理本质一致),也无直接继承关系。因此参数接收时应当使用接口作为类型
    3. 该方法由JDK原生生成,不需要额外导入包
  2. cglib动态代理:
    1. 被代理的目标类不需要实现接口
    2. 通过被代理的目标类生成并实现一个新类
    3. 该包被融入到SpringFramework下,不需要额外导入包

通常来说,有接口的使用JDK,无接口的使用cglib。但是往往不需要额外注意这些,因为Spring AOP会自动处理这些。实际使用时直接使用Spring AOP即可

4.1.3 AOP与OOP

OOP是指面向对象编程,AOP是指面向切面编程。AOP本质基于OOP,且是对OOP编程思路的一种补充。OOP是针对父类与子类的纵向编程思路,AOP是若干基于同一父类(父接口)的子类之间的纵向封装关系。

4.2 Spring AOP的相关术语

横切关注点
AOP面相切面编程所需要关注的,非核心代码的关注点。例如token校验、日志输出等。

通知和通知方法
每一个横切关注点上所插入的方法就叫做通知方法
通知是指调用通知方法的这个动作

通知主要有如下几种类型

  1. 前置通知(@Before):
    目标方法被调用之前的通知
  2. 返回通知(@AfterReturning):
    目标方法被正常执行完毕后的通知(出现异常时不通知)
  3. 异常通知(@AfterThrowing):
    目标方法发生异常时的通知
  4. 后置通知(@After):
    无论目标方法是否成功执行均会被调用的通知
  5. 环绕通知(@Around):
    就是不自带 try...catch...finally 逻辑了,把接口丢给你让你自己执行。具体可见环绕通知

连接点
被代理拦截到的点,是一个逻辑概念

切入点
被选中切入的连接点;切点一定是连接点。

切面
切面=切入点+通知

目标
指被代理的目标

代理
即生成的代理对象

织入
切点被配置的动作

4.2.1 通知类型及其连接点

如上一章节所述,通知主要有以下五种类型:

  1. 前置通知(@Before):
    目标方法被调用之前的通知
  2. 返回通知(@AfterReturning):
    目标方法被正常执行完毕后的通知(出现异常时不通知)
  3. 异常通知(@AfterThrowing):
    目标方法发生异常时的通知
  4. 后置通知(@After):
    无论目标方法是否成功执行均会被调用的通知
  5. 环绕通知(@Around):
    就是不自带 try...catch...finally 逻辑了,把接口丢给你让你自己执行。具体可见环绕通知

其调用逻辑如下方伪代码所示:

try {
	@Before
	method();
	@AfterReturning
} catch {
	@AfterThrowing
} finally {
	@After
}

4.3 使用Spring AOP完成面向切面编程

4.3.1 使用注解方式完成面向切面编程

使用注解方式进行面相切面编程的步骤主要如下:

  1. 定义通知方法
  2. 使用注解配置,配置切点表达式
  3. 补全注解,以加入容器和配置切面
  4. 开启Aspect注解支持

步骤1,定义通知方法:
按照需求定义方法即可,通常会将类名命名为 ${功能}Advance ,并放置于 advance 包下,例如增加日志功能:

public class LogAdvance {

	public void before() {
		System.out.println("@Before ...");
	}

	public void after() {
		System.out.println("@After ...");
	}

	public void afterThrowing() {
		System.out.println("@AfterThrowing ...");
	}
}

步骤2,使用注解配置和选中这些目标方法:

public class LogAdvance {
	@Before("execution(* indi.h13.ssserver.service.impl.*.*(..))")
	public void before() {
		System.out.println("@Before ...");
	}

	@After("execution(* indi.h13.ssserver.service.impl.*.*(..))")
	public void after() {
		System.out.println("@After ...");
	}
	
	@AfterThrowing("execution(* indi.h13.ssserver.service.impl.*.*(..))")
	public void afterThrowing() {
		System.out.println("@AfterThrowing ...");
	}
}

注:

  • "execution(* indi.h13.ssserver.service.impl.*.*(..))" 中的:
    • 第一个 * 表示忽略方法返回值类型
    • indi.h13.ssserver.service.impl 为包名
    • 第二个 * 表示匹配该包下的所有类
    • 第三个 * 表示匹配所有方法
    • (..) 表示忽略方法参数
    • 具体可见章节切点表达式

步骤3,补全注解,以加入容器和配置切面:
主要需要注意以下内容:

  1. 为该增强类增加以下两个注解
    1. @Aspect 表示该类是个切面
    2. @Component 将其放置于IoC容器中
      即:
@Component
@Aspect
public class LogAdvance {
	@Before("execution(* indi.h13.ssserver.service.impl.*.*(..))")
	public void before() {
		System.out.println("@Before ...");
	}

	@After("execution(* indi.h13.ssserver.service.impl.*.*(..))")
	public void after() {
		System.out.println("@After ...");
	}
	
	@AfterThrowing("execution(* indi.h13.ssserver.service.impl.*.*(..))")
	public void afterThrowing() {
		System.out.println("@AfterThrowing ...");
	}
}
  1. 是否在配置文件或配置类中选择扫描 advance 包。

步骤4,开启Aspect注解支持:

  1. 对于使用xml进行配置的,直接在SpringConfig中添加 <aop:aspectj-autoproxy /> 即可:
<?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">  
  
    <!-- 开启Aspectj注解支持 -->  
	<aop:aspectj-autoproxy />
</beans>
  1. 对于使用配置类进行配置的,需要在配置类前额外增加一个 @EnableAspectJAutoProxy 注解即可:
// 配置包扫描注解,`value = ` 可以省略
@ComponentScan(value = {...})
// 配置配置文件名注解,`value = ` 可以省略
@PropertySource(value = ...)
@Configuration
@EnableAspectJAutoProxy
public class JavaConfiguration {
}

4.3.2 获取通知节点信息

在完成上述章节的通知方法定义后,会直接面对如下需求:

  1. 获取目标方法信息(方法名、参数、访问修饰符、所属类...)
  2. 获取方法返回值
  3. 捕捉异常对象
4.3.2.1 获取目标方法信息

获取目标方法信息是在任何一个增强方法中都可以执行的
具体步骤是:

  1. 在通知方法中添加 JoinPoint 类的对象。
  2. 随后使用 JoinPoint 对象即可获取目标方法信息。
    Demo如下:
@Component
@Aspect
public class LogAdvance {
	@Before("execution(* indi.h13.ssserver.service.impl.*.*(..))")
	public void before(JoinPoint joinPoint) {
		// 1. 获取所属类的简称
		System.out.println("Class name: " + joinPoint.getTarget().getClass().getSimpleName());
		
		// 2. 获取所属类的全称
		System.out.println("Class name: " + joinPoint.getTarget().getClass().getName());

		// 3. 获取方法名
		System.out.println("Function name: " + joinPoint.getSignature().getName());

		// 4. 获取参数
		Object args[] = joinPoint.getArgs();

		// 5. 获取访问修饰符
		int modifiers = joinPoint.getSignature().getModifiers();
		System.out.println("Function modifiers: " + Modifier.toString(modifiers));
	}
}
4.3.2.2 获取方法返回值

获取方法返回值仅能在@AfterReturning增强中才可以获取
具体步骤如下:

  1. 在@AfterReturning的通知方法中增加一个 Object result
  2. 配置 returningresult
    1. 注解方式为: @AfterReturning(value="...", returning=result)
    具体Demo为:
@Component
@Aspect
public class LogAdvance {
	@AfterReturning(
		value = "execution(* indi.h13.ssserver.service.impl.*.*(..))",
		returning = result
	)
	public void afterReturning(Object result) {
		// ...
	}
}
4.3.2.3 获取方法异常对象

获取方法异常对象仅能在@AfterThrowing增强中才可以获取
具体步骤如下:

  1. 在@AfterThrowing的通知方法中增加一个 Throwable throwable
  2. 配置 throwingthrowable
    1. 注解方式为: @AfterThrowing(value = "...", throwing = throwable)
    具体Demo如下:
@Component
@Aspect
public class LogAdvance {
	@AfterThrowing(
		value = "execution(* indi.h13.ssserver.service.impl.*.*(..))",
		throwing = throwable
	)
	public void afterThrowing(Throwable throwable) {
		System.out.println("@AfterThrowing ...");
	}
}

4.3.3 切点表达式

4.3.3.1 切点表达式的基本语法

切点表达式的基本结构为:
Pasted image 20241019194134.png
注:

  1. 切点表达式可以分段为 ${权限+返回类型} ${方法所在全类名} ${方法名} (${参数}) ,其中:
    • ${权限+返回类型}不能只指定返回类型而通配修饰符,也不能指定修饰符通配返回类型,要通配就全通配,要指定就全指定,不能出现 * intprvate *
    • ${方法所在全类名} 段有以下特性:
      • 通配时有单层模糊和多层模糊两种
        1. 单层模糊: indi.h13.ssserver.*.services
        2. 多层模糊: indi..services但是 .. 不能开头,如果需要模糊匹配开头,可以将二者结合,写为 *..services
      • 和图中所示一样,类名可以部分匹配
    • (${参数}) 段有如下特殊写法:
      • (..) 表示任意参数,有没有都行
      • () 表示无参
      • (String..) 表示以String开头,后面有没有无所谓
      • (..int) 表示以int结尾,开头有没有无所谓
      • (String, int) 等表示具体参数
4.3.3.2 切点表达式的提取和复用

通常来说,在一个工程中会有多个通知方法被插入到同一个切点表达式所限定的一类切点中(例如上述章节的Demo中多次使用表达式 "execution(* indi.h13.ssserver.service.impl.*.*(..))" )。为了避免重复编写,统一管理,就有了切点表达式的提取和复用。
复用方式有如下几种:

  1. 同一类内提取
  2. 在一个项目中,全局内统一提取并维护
4.3.3.2.1 同一类内提取和复用

Demo如下:

@Component
@Aspect
public class LogAdvance {
	@Pointcut("execution(* indi.h13.ssserver.service.impl.*.*(..))")
	public void pc() { }

	// 同一类内可以直接指定方法名
	@Before("pc()")
	public void before() {
		System.out.println("@Before ...");
	}

	@After("pc()")
	public void after() {
		System.out.println("@After ...");
	}
	
	@AfterThrowing("pc()")
	public void afterThrowing() {
		System.out.println("@AfterThrowing ...");
	}
}
4.3.3.2.2 全局内同一提取和维护

通常考虑创建一个单独维护切点的类,统一维护切点。通常放置于 pointcuts 包中。

package indi.h13.pointcuts;  

@Component
public class LogPointCut {
	@Pointcut("execution(* indi.h13.ssserver.service.impl.*.*(..))")
	public void serviceImplPc() { }
}

注:

  1. 需要将该类加入IoC容器中。
  2. 需要配置注解扫描包。

随后在增强类中使用即可:

@Component
@Aspect
public class LogAdvance {

	// 在类外时使用方法名的全限定符
	@Before("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
	public void before() {
		System.out.println("@Before ...");
	}

	@After("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
	public void after() {
		System.out.println("@After ...");
	}
	
	@AfterThrowing("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
	public void afterThrowing() {
		System.out.println("@AfterThrowing ...");
	}
}

4.3.4 环绕通知

前面四个通知类型是基于 try { } catct { } finally { } 结构进行的,而环绕通知是不自带这些基本结构,把句柄丢给环绕通知方法,让开发者在环绕通知方法内自行调用随意切片的一种设计。

public class LogAroundAdvance {
	@Around("execution(* indi.h13.ssserver.service.impl.*.*(..))")
	public Object transaction(ProceedingJoinPoint joinPoint) {
		Object result;
		// Do anything...
		try {
			// Do anything...
			// Even without calling the objective function...
			result = joinPoint.proceed(joinPoint.getArgs());
			// Do anything...
		} catch (Throwable t) {
			// Do anything...
			throw t;
		} finally {
			// Do anything...
		}
		
		// Do anything...
		return result;
	}
}

4.3.5 切面优先级

无论对同一个组件方法添加了多少个增强代码,不同切面之间的优先级是固定的,即:

  1. 执行@Before
  2. 执行@AfterReturning @AfterThrowing
  3. 执行@After
    总体执行逻辑是和 try catch 一致的包裹结构:
    Pasted image 20241019234230.png

但是同一切面内的不同增强方法之间可以使用 @Order(value) 设置优先级,value值越低的越靠外执行,即:

  • value值低的前置切面( @Before )比value值高的前置切面先执行
  • value值低的后置切面( @Before )比value值高的后置切面后执行
    假设下方的代码:
@Component
@Aspect
@Order(10)
public class Advance1 {

	// 在类外时使用方法名的全限定符
	@Before("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
	public void before() {
		System.out.println("@Before ...");
	}

	@After("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
	public void after() {
		System.out.println("@After ...");
	}
	
	@AfterThrowing("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
	public void afterThrowing() {
		System.out.println("@AfterThrowing ...");
	}
}
@Component
@Aspect
@Order(7)
public class Advance2 {

	// 在类外时使用方法名的全限定符
	@Before("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
	public void before() {
		System.out.println("@Before ...");
	}

	@After("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
	public void after() {
		System.out.println("@After ...");
	}
	
	@AfterThrowing("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
	public void afterThrowing() {
		System.out.println("@AfterThrowing ...");
	}
}

注:

  • @Order 被省略时默认为int类型的最大值,即优先级最低
  • 上述切面的执行顺序为:
    1. Advavce2.before()
    2. Advavce1.before()
    3. method()
    4. Advavce1.after()
    5. Advavce2.after()
  • 一种应用场景:
    Pasted image 20241019234925.png

4.3.6 使用xml完成AOP配置

基本不用,了解即可。
步骤:

  1. 先准备一个增强类
public class LogAdvance {

	public void before() {
		System.out.println("@Before ...");
	}

	public void after() {
		System.out.println("@After ...");
	}

	@AfterThrowing(
		value = "execution(* indi.h13.ssserver.service.impl.*.*(..))",
		throwing = throwable
	)
	public void afterThrowing(Throwable throwable) {
		System.out.println("@AfterThrowing ...");
	}
}
  1. 注册组件
<?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. 注册组件 -->  
	<bean id="logAdvance" class="indi.h13.advances.LogAdvance"/>  
</beans>
  1. 注册切面标签,声明切点
<?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. 注册组件 -->  
	<bean id="logAdvance" class="indi.h13.advances.LogAdvance"/>  

	<!-- 2. 声明切点标签、引用增强对象 -->
	<!-- 等效于@Aspect注解 -->
	<aop:config>
		<!-- 2.1 声明切点标签 -->
		<!-- 等效于@Pointcut注解及其切点类 -->
		<aop:pointcut id="pc" expression="execution(* indi.h13.ssserver.service.impl.*.*(..))" />

		<!-- 2.2 引用增强对象 -->
		<!-- 等效于@Order注解 -->
		<aop:aspect ref="logAdvance" order="7">
			<!-- 等效于@Before注解 -->
			<aop:before method="before" pointcut-ref="pc"/>
			
			<!-- 也可以选择不引用切点 -->
			<!-- 等效于@AfterThrowing(value="",throwing="throwable")注解 -->
			<aop:after-throwing method="afterThrowing" pointcut="execution(* indi.h13.ssserver.service.impl.*.*(..))" throwing="throwable"/>
		</aop:aspect>
	</aop:config>
</beans>

5 TX声明式事务

5.1 事务管理所针对的问题及其Demo

考虑如下的一个业务逻辑:

// 用户注册操作
public void regist(String userName, String email, String phoneNumber) {
	try {
		User user = userMapper.createNewUser();
		user.updateUserName(userName);
		user.updateEmail(email);
		user.updatePhoneNumber(phoneNumber);
	} catch (Exception e) {
		if(e instanceof InvalidUserNameException) {
			userMapper.deregistration(userName);
		} else if(e instanceof IncorrectEmailException) {
			user.clearEmail();
		} else if(e instanceof IncorrectPhoneNumberException) {
			user.clearPhoneNumber();
		}
	}
}

可以发现这种连续的,且有先后要求的请求出现rollback时的逻辑较为复杂(尽管注册用户的时候一般不需要这么做),但是总体来说许多业务都可能用得到如下逻辑:
0. 响应超时回滚,以及撤销所有操作

  1. 尝试请求1,成功则继续,失败则取消操作1
  2. 尝试请求2,成功则继续,失败则rollback,撤销请求1
  3. 尝试请求3,成功则继续,失败则rollback,撤销请求1、2
  4. 尝试请求4,成功则继续,失败则rollback,撤销请求1、2、3
  5. ...
    可以发现,复杂逻辑的rollback是个复杂且容易疏漏的问题(其实以数据库来说,不commit就行)。同时精确时长的超时回滚也需要框架底层的支持。

此外,再考虑一个场景:

  • 假设 regist 正在依次执行上述四个操作的同时,还有其他的组件也在操作目标用户的数据。那如何解决并行化问题?

而Spring为了解决上述若干问题,给出了事务管理的一个框架(事务管理器)。
这个框架在项目中往往仅在数据库操作时使用,也就是项目中只需要一个事务管理器实例(不过也有多例的情况)。
事务管理器的使用步骤:

  1. 选择一个合适的事务管理器实现加入到IoC容器中
  2. 指定对应的方法添加到事务

先以单例的数据库操作事务为例,可以将上述的代码进行如下的实现:

  1. 确定选用jdbc操作数据库,实例化Druid连接池、jdbcTemplate,以及选用DataSourceTransactionManager管理JDBC事务
@Configuration
@ComponenScan("indi.h13")
@PropertySource(value = "classpath:jdbc.properties")
@EnableTransactionManagement
public class DataSourceConfig {
    // 实例化DruidDataSource,略
    @Bean
    public DataSource dataSource(...) { }

    /**
     * 实例化JdbcTemplate对象,并在UserMapper中用于数据库操作
     */
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource){
        JdbcTemplate jdbcTemplate = new JdbcTemplate();
        jdbcTemplate.setDataSource(dataSource);
        return jdbcTemplate;
    }
    
    /**
     * 装配事务管理实现对象
     */
    @Bean
    public TransactionManager transactionManager(DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }
}
  1. 在Mapper层实现对应数据库操作,完成UserMapper的编写
  2. 在Service层使用实物直接,选定需要管理的数据库操作事务:
public classs UserService {
	// 用户注册操作
	@Transactional
	public void regist(String userName, String email, String phoneNumber) {
	}
}
  1. 在数据库操作事务中只需要去操作数据库,不需要担心rollback问题(框架代为实现rollback):
public classs UserService {
	// 用户注册操作
	@Transactional
	public void regist(String userName, String email, String phoneNumber) {
		User user = userMapper.createNewUser();
		user.updateUserName(userName);
		user.updateEmail(email);
		user.updatePhoneNumber(phoneNumber);
	}
}

此时就已经实现了rollback操作,当其中某一步发生异常时,数据库就会自动回滚到全部请求开始之前的状况。

注意点:

  1. @Transactional 可以标注到类前,也可以标注到方法前。当标注到类前时,其下所有方法均为事务方法通常会将事务注解加到类前
  2. 因为大多数项目只需要一个事务管理器(通常是去管理数据库),因此 @Transactional 会自动指定项目中唯一的 TransactionManager
  3. 当项目中有多个事务管理器时,可以在注解内指定对应的事务管理器的组件名来指定对应的管理器。例如: @Transactional(transactionManager="transactionManager")

5.2 事务管理的基本特性

5.2.1 (多事务管理器时)选定特定的事务管理器完成实物

在注解内指定Bean ID即可,即:

public classs UserService {
	// 用户注册操作
	@Transactional(transactionManager="transactionManager")
	public void regist(String userName, String email, String phoneNumber) {
		User user = userMapper.createNewUser();
		user.updateUserName(userName);
		user.updateEmail(email);
		user.updatePhoneNumber(phoneNumber);
	}
}

5.2.2 只读模式

一般来说进行只读事务操作时,也不太需要事务管理器进行rollback。但是使用事务管理器可以保证该事务中只有只读事务。其特性有:

  1. 开启只读模式后如果有写入操作则会抛出异常
  2. 只读模式的运行速度会快一些。
    此时在注解中增加 readOnly = true 即可:
public classs UserService {
	// 用户注册操作
	@Transactional(readOnly = true)
	public void getUserInfo(int targetUid) {
		UserVo userVo = new UserVo();
		// BeanUtils.copyProperties(userService.getById(uid), userVo);
		userVo.userName = userMapper.getUserName();
		userVo.avatarId = userMapper.getAvatarId();
		...
	}
}

由于通常会将事务注解加到类前,因此需要再在方法前加一个只读限定的注解以优化性能

5.2.3 超时限制

在注解中增加 timeout=t 即可,单位为秒,类型为整数。默认值为-1,即永不超时。
当超时时抛出 TransactionTimeOutException 异常。

public classs UserService {
	// 用户注册操作
	@Transactional(readOnly = true, timeout = 1)
	public void getUserInfo(int targetUid) {
		UserVo userVo = new UserVo();
		// BeanUtils.copyProperties(userService.getById(uid), userVo);
		userVo.userName = userMapper.getUserName();
		userVo.avatarId = userMapper.getAvatarId();
		...
	}
}

需要注意的是,由于timeout自带默认值-1,因此若在类上标注了timeout,而在方法上因为其他方式又重新标注了一次Transactional注解,则此时的timeout会以方法上的注解为准
即此种情况下:

  • 若类上未标注timeout参数则timeout=-1,无限时长
  • 标定指定值的timeout后,以后续标定值为准

5.2.4 可选回滚

如上述章节所述,事务内部发生异常时事务管理器会自动进行回滚。但是默认时并不是所有异常都会出发回滚!默认时触发RuntimeException时才会进行回滚。
而注解中可以使用 rollbackFor=<? extends Throwable>[] 来指定需要回滚的异常,例如 rollbackFor=Throwable.class
此外,还可以用 noRollbackFor 来取消特定异常的回滚。noRollbackForrollbackFor 后判定。

5.2.5 事务隔离级别

考虑如下几个并发问题:

  • 脏读(Dirty Read):脏读发生在一个事务读取到另一个事务未提交的数据时。如果那个未提交的事务最终被回滚,则第一个事务读取到的数据实际上是从未存在过的。这可能导致数据不一致和业务逻辑的错误。
  • 不可重读(Non-repeatable Read):不可重复读发生在一个事务中多次读取同一数据时,在两次读取之间,另一个事务修改了该数据并提交,导致第一个事务在第一次和第二次读取中看到了不同的数据。
  • 幻读(Phantom Read):幻读与不可重复读类似,但幻读是指在一个事务内读取到了另一个事务新增的数据行。它通常发生在一个事务试图重复执行一个查询时,发现另一个事务已经插入了满足该查询条件的新记录。这种情况主要关注的是因为新插入的数据行导致的数据不一致。

而对于上述三个并发问题,并不是在所有场景中都需要完全避免上述三个问题,有些问题是可以接受的。对应的四个隔离级别分别为:

  1. 读未提交:允许读未提交的数据
  2. 读提交:不允许读未提交的数据
  3. 可重复读:保证在同一事务中两次读取到的数据是一致的
  4. 串行化:完全禁止并发,避免上述问题

Spring中的默认隔离级别为可重复读,但是推荐修改为读提交,这样性能会好一些。

具体的设置方法是在注解中设置 isolation 属性,可选项分别是:

  • Isolation.DEFAULT :可重复读
  • Isolation.READ_UNCOMMITTED
  • Isolation.READ_COMMITTED
  • Isolation.REPEATABLE_READ
  • Isolation.SERIALIZABLE

5.2.6 事务传播行为

现在一个事务内调用了两个方法,一个是业务方法,另外一个也是业务方法。
考虑如下的情况:

@Transactional
public void doResponse() {
	transactionA();
	transactionB();
}

并且假设事务 transactionB 执行时一定会失败,并思考 transactionA 的执行结果是否会被回退、以及回退与否是否可以选择。

而事务传播行为就是用于设定 transactionA 是否会被 transactionB 影响的一个特性,该选项通过注解中的 propagation 属性进行设置,其通常使用如下两个选项:

  • Propagation.REQUIRED :默认值,表示:
    • 若该事务被调用时有事务上下文,则该事务在原先事务的上下文中执行
    • 若该事务被调用时没有事务上下文,则创建新的事物上下文
  • Propagation.REQUIRES_NEW :表示:
    - 无论该事务被调用时有没有事务上下文,都会创建一个全新的事务上下文用于执行该事务
    - 该事务执行时会挂起原先的事务上下文(如果存在的话)
    - 该事务执行完毕后,销毁为这个事务创建的新事务上下文,并恢复原事务上下文(如果存在的话)
    (当然还有更多的选项)

接下来考虑如下的调用过程:

  1. 事务1,属性为Propagation.REQUIRED
  2. 事务2,属性为Propagation.REQUIRES_NEW
  3. 事务3,属性为Propagation.REQUIRED
  4. 事务4,属性为Propagation.REQUIRES_NEW
    那么
  5. 事务1被调用,属性为Propagation.REQUIRED:
    • 此时不存在事务上下文,创建事务上下文1
  6. 事务2被调用,属性为Propagation.REQUIRES_NEW
    • 此时挂起事务上下文1,创建事务上下文2,并在2中执行事务2
    • 事务2执行完毕后,销毁事务上下文2,恢复事务上下文1
  7. 事务3被调用,属性为Propagation.REQUIRED
    • 在事务上下文1中执行
  8. 事务4被调用,属性为Propagation.REQUIRES_NEW
    • 此时挂起事务上下文1,创建事务上下文3,并在2中执行事务4
    • 事务4执行完毕后,销毁事务上下文3,恢复事务上下文1

此时,若

  1. 事务1触发回滚,事务1会被回滚
  2. 若事务2触发回滚则事务1不会受到影响事务3也会继续推进
  3. 若事务3触发回滚则事务1会被回滚但是事务2不会受影响
  4. 若事务4触发回滚,则事务1、2、3都不会受影响

通常来说,子事务一般使用 Propagation.REQUIRED 设置事务传播行为,而父事务可以选择使用 try catch 来捕获子事务的异常,达到可控回滚。

总体来说,当前子事务期望前置事务回滚与否可以通过 propagation 进行设置,而子事务是否期望被后置事务回滚也是通过 propagation 设置。
而父事务可以通过 try catch 来强行控制回滚与否。
通常来说该考点会在面试时考察,实际使用中多数为默认回滚。