# SpringTest
**Repository Path**: zzzfatfish/spring-test
## Basic Information
- **Project Name**: SpringTest
- **Description**: SpringTest:专注于Spring框架的测试与学习,提供丰富的示例代码和最佳实践,帮助开发者快速掌握Spring应用开发技巧。
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2025-08-13
- **Last Updated**: 2025-08-18
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Spring 学习笔记
## 1. Spring 核心概念
### 1.1 简介
**Spring 的寓意**
Spring,意为“春天”,旨在为软件开发带来春天般的生机与活力。
**发展历史**
* **2002年**:Rod Johnson(悉尼大学音乐学博士)发布了 Spring 框架的雏形——`Interface21` 框架。
* **2004年**:在 `Interface21` 的基础上,经过重新设计和功能丰富,Spring 1.0 正式版发布。
* **核心理念**:Spring 的目标是让现有的 Java 技术(如 Java EE)更加易于使用。它本身是一个集大成者,通过整合各种优秀的开源框架,为开发者提供一站式的解决方案。
**经典技术栈**
* **SSH**: Struts2 + Spring + Hibernate
* **SSM**: Spring MVC + Spring + MyBatis
**相关资源**
* **官网**: [https://spring.io/](https://spring.io/)
* **项目主页**: [https://spring.io/projects/spring-framework](https://spring.io/projects/spring-framework)
* **官方文档**: [Spring Framework Documentation](https://docs.spring.io/spring-framework/docs/current/reference/html/)
* **Maven 仓库**: [https://mvnrepository.com/](https://mvnrepository.com/)
**Maven 核心依赖示例**
要在项目中使用 Spring,通常需要引入其模块依赖。例如,开发 Web 应用需要 `spring-webmvc`,使用 JDBC 需要 `spring-jdbc`。
```xml
org.springframework
spring-webmvc
5.3.23
org.springframework
spring-jdbc
5.3.23
```
### 1.2 优点
* **开源免费**:Spring 是一个开源的、免费的框架(容器)。
* **轻量级与非侵入式**:
* **轻量级**:指其核心库体积小,对系统资源占用少,并且可以按需引入模块。
* **非侵入式**:应用中的对象(POJO)无需继承 Spring 特定的类或实现接口,保持了代码的独立性。
* **两大核心**:
* **控制反转 (IoC - Inversion of Control)**:将对象的创建权交给 Spring 容器管理。
* **面向切面编程 (AOP - Aspect Oriented Programming)**:在不修改源码的情况下,对程序进行功能增强。
* **生态强大**:
* 提供强大的**事务管理**能力。
* 能够轻松**整合**几乎所有主流的第三方框架(如 MyBatis, Hibernate, Quartz 等)。
**总结:Spring 是一个以 IoC(控制反转)和 AOP(面向切面编程)为核心的,一站式的轻量级开源框架。**
### 1.3 组成 (Spring Framework Modules)
Spring 框架是一个分层架构,由多个功能明确的模块组成,可以根据项目需求按需引入。其核心模块主要包括:
1. **Core Container (核心容器)**
* `spring-core`, `spring-beans`, `spring-context`, `spring-expression`
* 这是 Spring 框架的基石,提供了 IoC 和 DI 的功能。`BeanFactory` 和 `ApplicationContext` 是其核心接口。
2. **AOP & Aspects (面向切面编程)**
* `spring-aop`, `spring-aspects`
* 提供了 AOP 的实现,允许开发者定义方法拦截器和切点,实现如日志、权限控制等横切关注点的功能。
3. **Data Access / Integration (数据访问与集成)**
* `spring-jdbc`: 简化了 JDBC 操作。
* `spring-orm`: 集成了主流的 ORM 框架,如 Hibernate、JPA。
* `spring-tx`: 提供了强大的声明式和编程式事务管理。
* `spring-jms`, `spring-messaging`: 用于消息传递。
4. **Web**
* `spring-web`: 提供了基础的 Web 功能,如文件上传、HTTP 客户端等。
* `spring-webmvc`: 包含了 Spring MVC 框架,用于构建 Web 应用程序。
* `spring-websocket`: 支持 WebSocket 通信。
### 1.4 拓展
**为什么现在还要学习 Spring?**
虽然 Spring Boot 和 Spring Cloud 是目前的主流,但它们都是构建在 Spring Framework 之上的。
* **Spring Boot**: 一个快速开发的脚手架,遵循“约定大于配置”的原则,极大地简化了 Spring 应用的配置和部署。
* **Spring Cloud**: 基于 Spring Boot 的一套微服务治理工具集。
**学习路径**:深入理解 **Spring** 和 **Spring MVC** 的原理,是掌握 **Spring Boot** 的前提。它是承上启下的关键。
**Spring 的“弊端”**
随着功能不断扩展,早期 Spring 的 XML 配置变得异常繁琐和复杂,导致项目难以维护,被戏称为“**配置地狱**”。Spring Boot 的出现正是为了解决这一问题,通过自动化配置让开发者回归到业务本身。
---
## 2. IoC 理论推导 (控制反转)
### 2.1 从一个实例开始理解
我们通过一个简单的例子来理解 IoC 的思想演变。假设我们有一个 `UserService`,它需要调用 `UserDao` 来获取数据。
**项目结构:**
* `UserDao` (接口)
* `UserDaoImpl` (实现类)
* `UserService` (接口)
* `UserServiceImpl` (实现类)
#### 步骤一:传统开发方式(主动创建依赖)
**UserDao 接口 & 实现**
```java
// com.github.subei.dao.UserDao
public interface UserDao {
void getUser();
}
// com.github.subei.dao.UserDaoImpl
public class UserDaoImpl implements UserDao {
@Override
public void getUser() {
System.out.println("默认实现:从数据库获取用户数据...");
}
}
```
**UserService 接口 & 实现**
```java
// com.github.subei.service.UserService
public interface UserService {
void getUser();
}
// com.github.subei.service.UserServiceImpl
import com.github.subei.dao.UserDao;
import com.github.subei.dao.UserDaoImpl;
public class UserServiceImpl implements UserService {
// 程序主动创建依赖对象,耦合度高
private UserDao userDao = new UserDaoImpl();
@Override
public void getUser() {
userDao.getUser();
}
}
```
**测试类**
```java
// MyTest.java
import com.github.subei.service.UserServiceImpl;
public class MyTest {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();
userService.getUser();
}
}
```
**问题分析**:在 `UserServiceImpl` 中,`userDao` 是通过 `new UserDaoImpl()` 直接创建的。如果此时客户需求变更,需要从 Oracle 数据库获取数据(`UserDaoOracleImpl`),我们必须修改 `UserServiceImpl` 的源代码。这种**高耦合**的方式在大型项目中是灾难性的。
#### 步骤二:改进方式(通过 Setter 注入)
为了解决上述问题,我们不再让 `UserServiceImpl` 自己创建 `UserDao`,而是提供一个 Setter 方法,让外部来决定使用哪个实现。
**修改 `UserServiceImpl.java`**
```java
// com.github.subei.service.UserServiceImpl
import com.github.subei.dao.UserDao;
public class UserServiceImpl implements UserService {
private UserDao userDao;
// 提供一个 set 方法,用于接收外部传入的依赖对象
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
@Override
public void getUser() {
userDao.getUser();
}
}
```
**修改测试类**
现在,我们可以在测试类中“注入”我们想要的 `UserDao` 实现。
```java
// 假设我们新增了一个 UserDaoMySqlImpl
public class UserDaoMySqlImpl implements UserDao {
@Override
public void getUser() {
System.out.println("新实现:从MySQL获取用户数据...");
}
}
// MyTest.java
import com.github.subei.dao.UserDaoMySqlImpl;
import com.github.subei.service.UserServiceImpl;
public class MyTest {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();
// 关键改变:我们在这里决定并“注入”具体的实现
userService.setUserDao(new UserDaoMySqlImpl());
userService.getUser();
}
}
```
**思想转变**:
* **之前**:`UserServiceImpl` 主动创建并**控制**它所依赖的 `UserDao` 对象。
* **现在**:`UserServiceImpl` 失去了对 `UserDao` 的创建控制权,它只能被动地接收。控制权**反转**到了调用方(`MyTest`)手中。
这种思想就是 **控制反转 (IoC)** 的雏形。程序从主动创建依赖,变成了被动接收依赖,从而大大降低了组件之间的耦合度。
### 2.2 IoC 的本质
**控制反转 (Inversion of Control, IoC)** 是一种**设计思想**,而不是一种具体的技术。它的核心是**将原本由程序代码直接操控的对象创建权,交由一个外部容器来管理**。
**依赖注入 (Dependency Injection, DI)** 是**实现 IoC 最常见的方式**。所谓“依赖注入”,就是指容器在运行时,动态地将某个对象所依赖的其它对象注入到该对象中。
**总结**:
1. **谁控制谁?**
* **传统方式**:应用程序的组件(如 `UserServiceImpl`)控制它所依赖的对象(`UserDaoImpl`)的创建。
* **IoC 方式**:IoC 容器(如 Spring 容器)控制了所有组件的创建。组件只负责声明自己的依赖,不负责创建。
2. **控制什么?**
* 主要控制外部资源的获取,例如对象实例、文件、数据库连接等。
3. **为何叫反转?**
* 因为获取依赖的方向反了。传统方式是组件主动去获取依赖,而 IoC 方式是容器将依赖“推送”或“注入”到组件中。
4. **Spring 如何实现 IoC?**
* Spring IoC 容器(`ApplicationContext`)在启动时,会读取配置元数据(XML 文件或注解)。
* 根据元数据创建和装配所有的对象(在 Spring 中称为 Bean)。
* 当应用程序需要某个对象时,直接从容器中获取即可,无需关心其创建过程和依赖关系。
通过 IoC,我们将对象的创建、管理和装配等繁杂工作交给了框架,使我们能更专注于业务逻辑的实现。这就是 Spring 框架的强大之处。
## 3. 第一个 Spring 程序 (HelloSpring)
通过一个简单的入门案例,我们来实际感受一下 Spring IoC 容器是如何工作的。
**步骤 1: 创建实体类 `Hello.java`**
这是一个普通的 POJO (Plain Old Java Object)。
```java
package com.github.subei.pojo;
public class Hello {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Hello{" +
"name='" + name + '\'' +
'}';
}
}
```
**步骤 2: 创建 Spring 配置文件 `beans.xml`**
这个 XML 文件是 Spring IoC 容器的“蓝图”,它描述了需要容器管理的对象(Bean)以及它们之间的关系。
```xml
```
**步骤 3: 编写测试类**
```java
import com.github.subei.pojo.Hello;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyTest {
public static void main(String[] args) {
// 1. 获取 Spring 的上下文对象 (IoC 容器)
// ClassPathXmlApplicationContext 会解析 beans.xml 文件, 创建并管理其中定义的 Bean
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
// 2. 从容器中获取我们需要的 Bean
// context.getBean("id"),参数即为 Spring 配置文件中 bean 的 id
Hello hello = (Hello) context.getBean("hello");
// 3. 使用对象
System.out.println(hello.toString());
}
}
```
### 核心概念回顾
* **`Hello` 对象是谁创建的?**
`Hello` 对象由 Spring IoC 容器创建,而不是我们手动 `new` 的。
* **`Hello` 对象的属性是怎么设置的?**
`name` 属性是在 `beans.xml` 中配置后,由 Spring 容器通过调用 `setName()` 方法注入的。
这个过程就是**控制反转 (IoC)** 的体现:
* **控制**:对 `Hello` 对象创建和管理的控制权。
* **反转**:控制权从我们的应用程序代码(手动 `new`)反转到了外部的 Spring 容器。
**依赖注入 (DI)** 则是实现 IoC 的一种方式,即容器将对象所需的依赖(如 `name` 属性值)通过 Setter 方法或构造函数注入进去。
**将 IoC 应用于上一节的例子**
现在,我们用 Spring 的方式来改造 `UserService` 和 `UserDao` 的例子,彻底实现解耦。
**`beans.xml` 配置**
```xml
```
**测试类**
```java
@Test
public void testUserService() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserServiceImpl service = (UserServiceImpl) context.getBean("userServiceImpl");
service.getUser(); // 输出:从MySQL获取用户数据...
}
```
**优势**:现在,如果想切换到 Oracle 数据库,我们**只需修改 XML 配置文件**,将 `ref` 的值从 `mysqlImpl` 改为 `oracleImpl`,而**无需改动任何一行 Java 代码**。这真正实现了“对象由 Spring 来创建、管理和装配”。
---
## 4. IoC 创建对象的方式
Spring 容器创建 Bean 主要通过反射机制,常见的方式有两种:
### 4.1 方式一:使用无参构造函数 (默认)
当我们在 XML 中只配置 `` 时,Spring 默认会调用该类的**无参构造函数**来创建实例。如果类中没有无参构造函数,Spring 容器会启动失败。
```java
public class User {
public User() {
System.out.println("User 的无参构造函数被调用!");
}
// ... getters and setters
}
```
**配置**
```xml
```
**结论**:在 `context.getBean("user")` 之前,当 `ApplicationContext` 初始化的那一刻,`User` 对象就已经通过无参构造函数被创建好了。
### 4.2 方式二:使用有参构造函数
如果希望 Spring 使用有参构造函数来创建对象,需要使用 `` 标签明确指定构造函数的参数。
```java
public class User {
private String name;
public User(String name) {
this.name = name;
System.out.println("User 的有参构造函数被调用!");
}
// ...
}
```
**配置方式**
```xml
```
**总结:在配置文件加载时,Spring 容器就会根据配置调用相应的构造函数来初始化 Bean。**
---
## 5. Spring 配置详解
### 5.1 别名 (``)
可以为一个 Bean 设置一个或多个别名,通过别名也能获取到同一个对象。
```xml
```
之后,`context.getBean("user")` 和 `context.getBean("userNew")` 获取的是同一个实例。
### 5.2 Bean 的基本配置 (`id`, `class`, `name`)
```xml
```
`context.getBean("user")`, `context.getBean("u2")`, `context.getBean("u3")`, `context.getBean("u4")` 都能获取到这个 Bean。
### 5.3 导入其他配置 (``)
在团队开发或大型项目中,为了便于管理,通常会将 Spring 配置拆分成多个文件(如按模块 `spring-dao.xml`, `spring-service.xml`)。可以使用 `` 标签将它们组合起来。
```xml
```
---
## 6. 依赖注入 (DI)
依赖注入是 IoC 的具体实现。它的核心思想是:
* **依赖**:Bean 的创建和运行依赖于 Spring 容器。
* **注入**:Bean 的属性(包括基本类型和对象类型)由 Spring 容器来设置和装配。
### 6.1 构造器注入
已在 `4.2` 节中介绍,通过 `` 标签实现。
### 6.2 Set 注入 (重点)
通过调用属性的 `setter` 方法进行注入,使用 `` 标签实现。这是最常用、最直观的注入方式。
**示例环境**
```java
// 地址类
public class Address {
private String address;
// ... getter, setter, toString
}
// 学生类,包含各种类型的属性
public class Student {
private String name; // 普通字符串
private Address address; // 其他 Bean 对象
private String[] books; // 数组
private List hobbies; // List 集合
private Map card; // Map 集合
private Set games; // Set 集合
private Properties info; // Properties
private String wife; // 用于测试 null
// ... 所有属性的 getter, setter, toString
}
```
**XML 配置大全**
```xml
《Java核心技术》
《深入理解JVM》
编程
游戏
LOL
CS:GO
2023001
男
```
### 6.3 拓展方式注入 (p-命名空间和c-命名空间)
为了简化 XML 配置,Spring 提供了 `p` 和 `c` 命名空间,它们是 `` 和 `` 的简写形式。
**使用前准备**:必须在 `` 根标签中引入对应的命名空间。
```xml
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="...">
```
### 6.4 Bean 的作用域 (`scope`)
在Spring中,那些组成应用程序的主体及由Spring IoC容器所管理的对象,被称之为bean。简单地讲,bean就是由IoC容器初始化、装配及管理的对象。
Bean 的作用域定义了 Spring 容器如何创建和管理 Bean 实例的生命周期。通过在 `` 标签中设置 `scope` 属性来指定。
| Scope | 描述 |
| --------------- | ------------------------------------------------------------ |
| **`singleton`** | **(默认)** 在整个 Spring IoC 容器中,该 Bean 只有一个实例。 |
| **`prototype`** | 每次请求(如 `getBean()`)时,都会创建一个新的 Bean 实例。 |
| `request` | 每次 HTTP 请求,创建一个新实例。仅在 Web 应用中有效。 |
| `session` | 每个 HTTP Session,创建一个新实例。仅在 Web 应用中有效。 |
| `application` | 在 `ServletContext` 的生命周期内,只创建一个实例。仅在 Web 应用中有效。 |
| `websocket` | 在 `websocket` 的生命周期内,只创建一个实例。仅在 Web 应用中有效。 |
**1. Singleton (单例模式)**
这是 Spring 的默认行为。容器启动时就创建好实例,以后每次获取都是同一个。
```xml
```
测试:
```java
User user1 = context.getBean("user", User.class);
User user2 = context.getBean("user", User.class);
System.out.println(user1 == user2); // 输出: true
```
**2. Prototype (原型模式)**
每次从容器获取时,都会创建一个全新的对象。适用于有状态的 Bean。
```xml
```
测试:
```java
User user1 = context.getBean("user", User.class);
User user2 = context.getBean("user", User.class);
System.out.println(user1 == user2); // 输出: false
```
**3. Request**
Spring 容器通过为每个 HTTP 请求使用loginAction bean 定义来创建LoginAction bean 的新实例。也就是说,loginAction bean 的作用域是 HTTP 请求级别。您可以根据需要更改创建实例的内部状态,因为从同一loginAction bean 定义创建的其他实例看不到这些状态更改。它们特定于单个请求。当请求完成处理时,将限制作用于该请求的 Bean。
考虑以下 XML 配置来定义 bean :
```xml
```
使用注解驱动的组件或 Java 配置时,可以使用@RequestScope注解 将组件分配给request范围。以下示例显示了如何执行此操作:
```java
@RequestScope
@Component
public class LoginAction {
// ...
}
```
**4、Section**
当一个bean的作用域为Session,表示在一个HTTP Session中,一个bean定义对应一个实例。该作用域仅在基于web的Spring ApplicationContext情形下有效。
考虑下面bean定义:
```xml
```
在单个 HTTP Session的生存期内,Spring 容器通过使用userPreferences bean 定义来创建UserPreferences bean 的新实例。换句话说,userPreferences bean 的作用域实际上是 HTTP Session级别。与请求范围的 Bean 一样,您可以根据需要任意更改所创建实例的内部状态,因为知道其他 HTTP Session实例(也使用从相同userPreferences Bean 定义创建的实例)不会看到这些状态更改,因为它们特定于单个 HTTP Session。当最终丢弃 HTTP Session时,也将丢弃作用于该特定 HTTP Session的 bean。
```java
@SessionScope
@Component
public class UserPreferences {
// ...
}
```
---
## 7. 自动装配 (Autowiring)
自动装配是 Spring IoC 容器的一项强大功能,它可以自动满足 Bean 之间的依赖关系,减少 XML 中的显式配置。
### 7.1 XML 自动装配
通过在 `` 标签上设置 `autowire` 属性实现。
**环境准备**
```java
public class Cat { public void shout() { System.out.println("喵~"); } }
public class Dog { public void shout() { System.out.println("汪~"); } }
public class People {
private Cat cat;
private Dog dog;
private String name;
// ... getters and setters and toString
}
```
**`beans.xml`**
```xml
```
* **`autowire="byName"`**: Spring 会在容器中查找 **id** 与 `People` 类中**属性名**(`cat`, `dog`)相同的 Bean,并进行注入。
* **`autowire="byType"`**: Spring 会在容器中查找与 `People` 类中**属性类型**(`Cat`, `Dog`)相匹配的 Bean,并进行注入。
* **注意**:使用 `byType` 时,容器中同类型的 Bean 必须是唯一的,否则会抛出异常。
```java
public class MyTest {
@Test
public void test(){
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
People people = context.getBean("people", People.class);
people.getCat().shout();
people.getDog().shout();
}
}
```
### 7.2 使用注解实现自动装配
注解是目前更主流的自动装配方式,它将配置信息直接写在 Java 类中,更为便捷。
**步骤 1:开启注解支持**
在 XML 配置文件中,需要添加 `context` 命名空间,并开启注解扫描。
```xml
```
**步骤 2:在类中使用注解**
```java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import javax.annotation.Resource;
public class People {
@Autowired // 1. 使用 @Autowired
@Qualifier("cat") // 2. 当有多个同类型Bean时,用 @Qualifier 指定名称
private Cat cat;
// @Autowired
// private Dog dog;
@Resource(name = "dog") // 3. 使用 @Resource (更推荐)
private Dog dog;
// ...
}
```
#### `@Autowired` vs `@Resource`
这是非常重要的一个区别点:
| 特性 | `@Autowired` (Spring 提供) | `@Resource` (JSR-250, Java 标准) |
| :----------- | :---------------------------------------------------- | :-------------------------------------------------- |
| **装配顺序** | 1. **按类型 (`byType`)** 在容器中查找。 | 1. **按名称 (`byName`)** 查找。 |
| | 2. 如果找到多个,再按名称 (`byName`) 查找。 | 2. 如果按名称找不到,再**按类型 (`byType`)** 查找。 |
| | 3. 如果仍未确定,可配合 `@Qualifier("beanId")` 使用。 | 3. 如果按类型也找不到或找到多个,则报错。 |
| **依赖** | 强依赖 Spring 框架。 | 是 Java 标准,减少了对 Spring 的耦合。 |
| **常用场景** | 简单场景,或需要 `@Qualifier` 精准控制时。 | **推荐使用**,其装配顺序更符合直觉,更明确。 |
* `@Autowired(required = false)`:可以标注某个属性不是必须的,如果容器中找不到对应的 Bean,该属性为 `null` 而不会报错。
**观点**:有人认为“注解一时爽,维护火葬场”,是因为当项目庞大、依赖关系复杂时,注解分散在各个类中,不如 XML 集中管理来得清晰。但在现代开发中,注解的便利性已成为主流,配合良好的设计可以很好地管理。
---
## 8. 使用注解开发(完全体)
之前的注解只是用于自动装配,Bean本身还是在XML中定义的。现在我们学习使用注解来**定义Bean**,从而可以完全替代XML。
**步骤 1:开启组件扫描**
使用 `` 来代替 ``。它不仅会开启注解支持,还会扫描指定包下的类,并将带有特定注解的类自动注册为 Bean。
```xml
```
**步骤 2:使用注解定义 Bean 及其属性**
### 8.1 定义 Bean (`@Component` 及其衍生注解)
* `@Component`: 通用的组件注解,表示这个类是一个由 Spring 管理的 Bean。默认的 beanId 是类名首字母小写(如 `User` -> `user`)。
* `@Component("customId")` 可以自定义 beanId。
为了更好地标识分层架构,`@Component` 有三个衍生注解,它们在功能上完全相同,但语义更清晰:
* `@Repository`: 用于标注数据访问层 (DAO) 的组件。
* `@Service`: 用于标注业务逻辑层 (Service) 的组件。
* `@Controller`: 用于标注表现层 (Controller) 的组件。
```java
import org.springframework.stereotype.Component;
@Component // 等价于
public class User {
// 相当于
@Value("subeiLY")
public String name;
// ...
}
```
### 8.2 注入属性 (`@Value`)
`@Value` 注解用于注入基本类型和 `String` 类型的值。
```java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class User {
// 相当于
@Value("subeiLY")
public String name;
}
```
### 8.3 注入对象
使用 `@Autowired` 或 `@Resource`,与第 7 节相同。
### 8.4 定义作用域 (`@Scope`)
使用 `@Scope` 注解来定义 Bean 的作用域。
```java
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype") // 等价于
public class User { ... }
```
### 8.5 小结:XML vs. 注解
* **XML**:配置集中,一目了然,适用于所有场景。但当 Bean 数量多时,会很繁琐。
* **注解**:配置分散在代码中,开发便捷。但对于第三方库的类(我们无法修改源码),则无法使用注解。
**最佳实践(混合使用)**:
* **XML**:负责整体配置,如数据源、事务管理器、组件扫描 `` 等。
* **注解**:负责业务类(Controller, Service, DAO)的 Bean 定义和依赖注入。
---
## 9. 使用 Java 配置 (JavaConfig)
从 Spring 3.0 开始,官方提供了完全基于 Java 类来进行配置的方式,可以彻底摆脱 XML 文件。
**步骤 1:创建配置类**
配置类使用 `@Configuration` 标注,它本身也是一个 `@Component`。
```java
package com.github.subei.config;
import com.github.subei.pojo.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration // 声明这是一个配置类,替代 beans.xml
@ComponentScan("com.github.subei.pojo") // 扫描组件,等同于
@Import(AnotherConfig.class) // 导入其他配置类,等同于
public class MyConfig {
// @Bean 注解表示这个方法将返回一个对象,该对象将被注册为 Spring 容器中的 Bean。
// 方法名 `getUser` 默认成为 bean 的 id。
// 返回值类型 `User` 相当于 标签的 class 属性。
@Bean
public User getUser() {
return new User(); // 返回要注入到容器中的对象
}
}
```
**步骤 2:创建实体类(可以与JavaConfig配合使用)**
```java
package com.github.subei.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
// @Component注解可以让 @ComponentScan 扫描到
@Component
public class User {
private String name;
public String getName() { return name; }
@Value("KANGSHIFU") // 注入属性值
public void setName(String name) { this.name = name; }
// ... toString
}
```
**步骤 3:测试**
当完全使用 Java 配置时,需要用 `AnnotationConfigApplicationContext` 来加载容器。
```java
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MyTest {
public static void main(String[] args) {
// 通过 AnnotationConfigApplicationContext 来获取容器,参数是配置类的 Class 对象
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
// beanId 默认为方法名 "getUser"
User user = (User) context.getBean("getUser");
System.out.println(user.getName());
}
}
```
---
好的,我已经帮你把笔记重新整理和排版,并针对你遇到的问题进行了一些补充说明。我保留了你所有的原始内容,包括你的个人备注,只是让整体结构更清晰、更易于阅读。
---
## 10、代理模式
> **为什么要学习代理模式?**
>
> 因为这就是 Spring AOP 的底层实现原理!
**代理模式的分类:**
* **静态代理**
* **动态代理**
### 10.1 静态代理
#### 角色分析
* **抽象角色 (Subject)**:一般会使用接口或者抽象类来定义。
* **真实角色 (Real Subject)**:被代理的角色,真正执行业务逻辑的类。
* **代理角色 (Proxy)**:代理真实角色,在真实角色执行前后,可以附加一些操作。
* **客户 (Client)**:访问代理角色的人。
#### 代码实现
**1. 接口 (Rent.java)**
```java
package com.github.subei.demo;
// 租房
public interface Rent {
public void rent();
}
```
**2. 真实角色 (Host.java)**
```java
package com.github.subei.demo;
// 房东
public class Host implements Rent{
public void rent(){
System.out.println("房东要出租房子!");
}
}
```
**3. 代理角色 (Proxy.java)**
```java
package com.github.subei.demo;
public class Proxy implements Rent { // 注意:代理类也应该实现同一个接口
private Host host;
public Proxy() {}
public Proxy(Host host) {
this.host = host;
}
public void rent(){
seeHouse();
host.rent(); // 调用真实角色的方法
contract();
fare();
}
// 看房
public void seeHouse(){
System.out.println("中介带你看房!");
}
// 收中介费
public void fare(){
System.out.println("收中介费!");
}
// 签合同
public void contract(){
System.out.println("和你签合同!");
}
}
```
**4. 客户端 (Client.java)**
```java
package com.github.subei.demo;
public class Client {
public static void main(String[] args) {
// 房东要租房子
Host host = new Host();
// 代理,中介帮房东租房子,但是代理角色一般会有一些附属操作!
Proxy proxy = new Proxy(host);
// 我们不直接找房东,而是直接找中介租房
proxy.rent();
}
}
```
#### 优缺点
* **优点:**
* 可以使得我们的真实角色更加纯粹,不再去关注一些公共的业务。
* 公共的业务由代理来完成,实现了业务的分工。
* 公共业务发生扩展时,方便集中管理。
* **缺点:**
* 一个真实角色就会产生一个代理角色,代码量会翻倍,开发效率会变低。
### 10.2 静态代理再理解
以用户管理业务为例,日志功能就是可以被代理的公共业务。
#### 代码实现
**1. 抽象角色 (UserService.java)**
```java
package com.github.subei.demo2;
// 实现增删改查业务
public interface UserService {
void add();
void delete();
void update();
void query();
}
```
**2. 真实角色 (UserServiceImpl.java)**
```java
package com.github.subei.demo2;
public class UserServiceImpl implements UserService {
public void add() {
System.out.println("添加用户");
}
public void delete() {
System.out.println("删除用户");
}
public void update() {
System.out.println("更新用户");
}
public void query() {
System.out.println("查询用户");
}
}
```
**3. 代理角色 (UserServiceProxy.java)**
```java
package com.github.subei.demo2;
public class UserServiceProxy implements UserService {
private UserServiceImpl userService;
public void setUserService(UserServiceImpl userService) {
this.userService = userService;
}
public void add() {
log("add");
userService.add();
}
public void delete() {
log("delete");
userService.delete();
}
public void update() {
log("update");
userService.update();
}
public void query() {
log("query");
userService.query();
}
// 日志方法
public void log(String msg){
System.out.println("执行了 " + msg + " 方法");
}
}
```
**4. 客户端 (Client.java)**
```java
package com.github.subei.demo2;
public class Client {
public static void main(String[] args) {
// 真实业务对象
UserServiceImpl userService = new UserServiceImpl();
// 代理类
UserServiceProxy proxy = new UserServiceProxy();
// 设置要代理的真实对象
proxy.setUserService(userService);
proxy.add();
proxy.query();
}
}
```
> **思考:** 这种开发模式像是纵向的业务开发中,切入了横向的功能(如日志)。
>
> 我们想要静态代理的好处,又不想要它的缺点,于是就有了 **动态代理**!
### 10.3 动态代理
* 动态代理和静态代理的角色一样。
* 动态代理的代理类是**动态生成的**,不是我们直接写好的。
* **动态代理分类:**
* **基于接口的动态代理** — JDK 动态代理【本例使用】
* **基于类的动态代理** — CGLIB
* **Java 字节码实现** — Javassist
> 需要了解两个核心类: `java.lang.reflect.Proxy` 和 `java.lang.reflect.InvocationHandler`。
#### 代码实现
**1. 抽象角色 (Rent.java)**
```java
package com.github.subei.demo3;
public interface Rent {
void rent();
}
```
**2. 真实角色 (Host.java)**
```java
package com.github.subei.demo3;
public class Host implements Rent {
public void rent(){
System.out.println("房东要出租房子!");
}
}
```
**3. 代理处理程序 (ProxyInvocationHandler.java)**
```java
package com.github.subei.demo3;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 会用这个类,自动生成代理类
public class ProxyInvocationHandler implements InvocationHandler {
// 被代理的接口
private Rent rent;
public void setRent(Rent rent){
this.rent = rent;
}
// 生成得到代理类
public Object getProxy(){
return Proxy.newProxyInstance(
this.getClass().getClassLoader(),
rent.getClass().getInterfaces(),
this);
}
// 处理代理实例,并返回代理结果
// 这个方法是代理对象调用任何接口方法时都会执行的
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 动态代理的本质就是利用反射机制
seeHouse();
// 调用真实对象的方法
Object result = method.invoke(rent, args);
fare();
return result;
}
// 附加操作
public void seeHouse(){
System.out.println("中介带你看房!");
}
public void fare(){
System.out.println("收中介费!");
}
}
```
**4. 客户端 (Client.java)**
```java
package com.github.subei.demo3;
public class Client {
public static void main(String[] args) {
// 真实角色
Host host = new Host();
// 代理角色:现在没有具体的代理类,只有一个处理器
ProxyInvocationHandler handler = new ProxyInvocationHandler();
// 通过调用程序来处理我们要调用的接口对象!
handler.setRent(host);
// 动态生成对应的代理类!
Rent proxy = (Rent) handler.getProxy();
proxy.rent();
}
}
```
> **核心:** 一个动态代理处理器,一般代理某一类业务,可以代理实现了同一接口的多个类。
### 10.4 动态代理再理解
我们可以编写一个通用的动态代理处理器,让它能代理任何对象。
#### 通用处理器
```java
package com.github.subei.Demo4;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 通用的动态代理处理器
public class ProxyInvocationHandler implements InvocationHandler {
// 被代理的对象,设置为Object类型
private Object target;
public void setTarget(Object target){
this.target = target;
}
// 生成得到代理类
public Object getProxy(){
return Proxy.newProxyInstance(
this.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}
// 处理代理实例,并返回代理结果
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log(method.getName());
Object result = method.invoke(target, args);
return result;
}
public void log(String methodName){
System.out.println("执行了 " + methodName + " 方法");
}
}
```
#### 测试
```java
package com.github.subei.Demo4;
import com.github.subei.demo2.UserService;
import com.github.subei.demo2.UserServiceImpl;
public class Client {
public static void main(String[] args) {
// 真实角色
UserServiceImpl userService = new UserServiceImpl();
// 代理角色处理器
ProxyInvocationHandler pih = new ProxyInvocationHandler();
// 设置要代理的对象
pih.setTarget(userService);
// 动态生成代理类!
UserService proxy = (UserService)pih.getProxy();
proxy.add();
}
}
```
#### 动态代理的好处
* 使得我们的真实角色更加纯粹,不再关注公共业务。
* 公共业务由代理完成,实现了业务分工。
* 公共业务发生扩展时,方便集中管理。
* 一个动态代理处理器可以代理多个类,只要它们实现了接口。
---
## 11、AOP(TODO:未手敲)
### 11.1 什么是AOP
AOP(Aspect Oriented Programming),意为:**面向切面编程**。通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
AOP是OOP(面向对象编程)的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
### 11.2 AOP在Spring中的作用
提供声明式事务;允许用户自定义切面。
**AOP 核心概念:**
* **横切关注点 (Cross-cutting concerns)**:
跨越应用程序多个模块的功能。即与我们业务逻辑无关,但我们又需要关注的部分,就是横切关注点。如:**日志、安全、缓存、事务**等等。
* **切面 (Aspect)**:
横切关注点被模块化的特殊对象。在代码中通常是一个**类**。
* **通知 (Advice)**:
切面必须要完成的工作,即切面类中的**方法**。
* **目标 (Target)**:
被通知的对象,即被代理的真实对象。
* **代理 (Proxy)**:
向目标对象应用通知之后创建的对象。
* **切入点 (Pointcut)**:
切面通知**执行的“地点”**的定义。
* **连接点 (JoinPoint)**:
与切入点匹配的执行点,在Spring中,连接点就是**方法的执行**。
> **Spring AOP 中,通过 Advice 定义横切逻辑,Spring 支持 5 种类型的 Advice。**
>
> **核心思想:** AOP 就是在不改变原有代码的情况下,去增加新的功能。
### 11.3 使用Spring实现AOP
#### 1. 导入AOP依赖
使用AOP织入,需要导入`aspectjweaver`依赖包。
```xml
org.aspectj
aspectjweaver
1.9.4
```
#### 方式一:使用Spring原生API接口
**1. 业务接口和实现类**
```java
// UserService.java
package com.github.subei.service;
public interface UserService {
void add();
void delete();
void select();
void update();
}
// UserServiceImpl.java
package com.github.subei.service;
public class UserServiceImpl implements UserService{
public void add() { System.out.println("增加了一个用户"); }
public void delete() { System.out.println("删除了一个用户"); }
public void select() { System.out.println("查询了一个用户"); }
public void update() { System.out.println("更新了一个用户"); }
}
```
**2. 前置/后置增强类**
```java
// Log.java (前置通知)
package com.github.subei.log;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;
public class Log implements MethodBeforeAdvice {
// method: 要执行的目标对象的方法
// args: 参数
// target: 目标对象
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println(target.getClass().getName() + "的" + method.getName() + "方法被执行了!");
}
}
// AfterLog.java (后置通知)
package com.github.subei.log;
import org.springframework.aop.AfterReturningAdvice;
import java.lang.reflect.Method;
public class AfterLog implements AfterReturningAdvice {
// returnValue: 返回值
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
System.out.println("执行了" + target.getClass().getName() + "的" + method.getName() + "方法,返回结果为:" + returnValue);
}
}
```
**3. Spring配置 (applicationContext.xml)**
```xml
```
**4. 测试**
```java
import com.github.subei.service.UserService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyTest {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 动态代理代理的是接口,所以要用接口类型接收
UserService userService = context.getBean("userService", UserService.class);
userService.select();
}
}
```
#### 方式二:自定义类来实现AOP
**1. 编写自定义切面类 (POJO)**
```java
package com.github.subei.diy;
public class DiyPointCut {
public void before(){
System.out.println("---------方法执行前---------");
}
public void after(){
System.out.println("---------方法执行后---------");
}
}
```
或:
```java
public class DiyPointCut {
// "前置通知"方法现在可以接收一个JoinPoint对象
public void before(JoinPoint jp) {
System.out.println("---------方法执行前---------");
// 1. 获取目标对象
System.out.println("目标对象: " + jp.getTarget());
// 2. 获取方法签名,从而得到方法名和类名
System.out.println("拦截的方法: " + jp.getSignature().getName());
System.out.println("方法所属类: " + jp.getSignature().getDeclaringTypeName());
// 3. 获取方法的参数
System.out.println("方法参数: " + Arrays.toString(jp.getArgs()));
}
// "后置通知"方法同样可以获取这些信息
public void after(JoinPoint jp) {
System.out.println("---------方法执行后---------");
System.out.println("方法 " + jp.getSignature().getName() + " 执行完毕。");
}
}
```
**2. Spring配置**
```xml
```
**3. 测试 (同上)**
#### 方式三:使用注解实现
**1. 编写注解实现的切面类**
```java
package com.github.subei.diy;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
// 使用注解方式实现AOP
@Aspect // 标注这个类是一个切面
public class AnnotationPointCut {
@Before("execution(* com.github.subei.service.UserServiceImpl.*(..))")
public void before(){
System.out.println("---------方法执行前(注解)---------");
}
@After("execution(* com.github.subei.service.UserServiceImpl.*(..))")
public void after(){
System.out.println("---------方法执行后(注解)---------");
}
// 在环绕增强中,我们可以给定一个参数,代表我们要获取处理切入的点
@Around("execution(* com.github.subei.service.UserServiceImpl.*(..))")
public void around(ProceedingJoinPoint jp) throws Throwable {
System.out.println("环绕前");
Signature signature = jp.getSignature(); // 获得签名
System.out.println("签名: " + signature);
// 执行目标方法: proceed
Object proceed = jp.proceed();
System.out.println("环绕后");
System.out.println("执行结果: " + proceed);
}
}
```
**2. Spring配置文件**
```xml
```
#### **知识点:``**
* 通过AOP命名空间的 `` 声明,可以自动为Spring容器中那些配置了 `@AspectJ` 切面的bean创建代理,织入切面。
* 它有一个 `proxy-target-class` 属性,默认为 `false`:
* `proxy-target-class="false"` (默认): 使用 **JDK动态代理** 织入增强。目标类必须实现接口。
* `proxy-target-class="true"`: 使用 **CGLIB动态代理** 技术织入增强。即使目标类没有实现接口,也可以创建代理。
* **注意:** 即使 `proxy-target-class` 设置为 `false`,如果目标类没有声明任何接口,Spring 将自动切换到使用 CGLIB。
### 11.4 三种实现 AOP 方式的对比
| 对比维度 (Feature) | 方式一:Spring API接口 | 方式二:自定义类 + XML配置 | 方式三:注解方式 (@AspectJ) |
| ------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| **实现方式** | 切面类必须实现Spring特定的接口,如 `MethodBeforeAdvice`。 | 切面类是一个普通的Java类 (POJO),不需要实现任何接口。 | 切面类是一个普通的Java类 (POJO),但使用 `@Aspect` 注解标识。 |
| **与Spring框架的耦合度** | **高**。代码强依赖于`org.springframework.aop.*`包,可移植性差。 | **低**。切面类本身完全不依赖Spring,可以独立存在。 | **中等**。依赖于`org.aspectj.*`注解包,但与Spring核心API解耦。 |
| **配置方式** | 完全通过XML配置。使用``标签将通知和切入点绑定。 | 完全通过XML配置。使用``标签引用切面Bean,并指定方法。 | XML中只需开启注解支持``,具体逻辑在Java类中通过注解完成。 |
| **易用性与可读性** | **差**。代码最繁琐,需要为不同类型的通知创建不同的类,结构分散。 | **中等**。逻辑与配置分离,需要来回查看XML和Java文件才能理解整体。 | **好**。逻辑、切入点、通知类型都集中在一个类中,代码即配置,内聚性高,可读性最好。 |
| **功能强大性** | **较弱**。仅提供基本的前置、后置等通知,功能有限。 | **较强**。支持前置、后置、环绕、异常等所有通知类型,但配置在XML中。 | **最强**。完全支持AspectJ的所有功能,特别是`@Around`环绕通知,可以精确控制目标方法的执行。 |
| **当前主流用法** | 已过时,基本不再使用。 | 在一些需要将AOP配置与业务代码完全解耦的遗留项目中可能见到。 | **绝对主流**,是当前Spring/Spring Boot项目开发的首选和推荐方式。 |
#### 相同点 (Core Principles)
1. **核心思想一致**:三种方式都是为了实现AOP(面向切面编程),将横切关注点(如日志、事务)与业务逻辑代码分离。
2. **底层实现一致**:它们的底层都依赖于Spring在运行时创建**动态代理**对象(JDK动态代理或CGLIB代理)来织入切面逻辑。
3. **依赖一致**:都需要在项目中引入 `aspectjweaver` 这个依赖包。
4. **切入点表达式一致**:定义切入点时,使用的 `execution()` 表达式语法是完全相同的。
#### 不同点 (Evolution & Style)
1. **最大区别在于“耦合度”和“配置风格”**:
* **方式一**是**强耦合、纯XML配置**。代码和Spring API绑死。
* **方式二**是**低耦合、纯XML配置**。代码是干净的POJO,但AOP的“身份”和行为完全由外部XML赋予。
* **方式三**是**中等耦合、注解驱动**。代码通过注解自我声明其AOP“身份”和行为,XML只负责开启总开关。
2. **代码的侵入性不同**:
* 方式一侵入性最强,因为它强制你的类去实现它的接口。
* 方式二和方式三对业务代码都是非侵入式的,这也是AOP提倡的。
3. **发展趋势和推荐度不同**:
这三种方式清晰地展示了Spring AOP的演进路线:从早期与框架紧密绑定的API,到配置与代码分离,再到最终使用注解将配置与逻辑内聚。**方式三(注解)无疑是目前最佳的实践**,因为它兼顾了低耦合、高可读性和强大的功能。
**简单来说,你可以这样理解它们的演进:**
* **方式一**:“你(代码)必须听我的(框架),按我的规矩来写。”
* **方式二**:“你(代码)做你自己的事,我(XML)来告诉你什么时候该做什么。”
* **方式三**:“你(代码)自己决定自己该做什么,并告诉大家(通过注解),我(框架)负责让你生效就行。”
---
## 12、整合MyBatis
### 整合步骤概览
1. **导入相关Jar包**
2. **编写配置文件**
3. **编写代码与测试**
#### 1. 导入Maven依赖
```xml
junit
junit
4.12
org.mybatis
mybatis
3.5.3
mysql
mysql-connector-java
5.1.47
org.springframework
spring-webmvc
5.2.12.RELEASE
org.springframework
spring-jdbc
5.1.10.RELEASE
org.aspectj
aspectjweaver
1.9.6
org.mybatis
mybatis-spring
2.0.6
```
#### 2. 配置Maven静态资源过滤
为了确保 `src/main/java` 目录下的 `.xml` 和 `.properties` 文件能被正确打包,需要在 `pom.xml` 中添加以下配置:
```xml
src/main/java
**/*.properties
**/*.xml
true
```
### 12.1 回忆MyBatis
在整合前,我们先回顾一下原生MyBatis的开发流程。
#### 1. 实体类 (User.java)
```java
package com.github.subei.pojo;
import lombok.Data;
@Data
public class User {
private int id;
private String name;
private String pwd;
// ... Constructors, Getters, Setters, toString() ...
}
```
#### 2. MyBatis核心配置文件 (mybatis-config.xml)
```xml
```
#### 3. Mapper接口 (UserMapper.java)
```java
package com.github.subei.mapper;
import com.github.subei.pojo.User;
import java.util.List;
public interface UserMapper {
List selectUser();
}
```
#### 4. Mapper XML文件 (UserMapper.xml)
```xml
```
#### 5. 测试类
```java
public class MyTest {
@Test
public void selectUser() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
try {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
List userList = mapper.selectUser();
for(User user : userList){
System.out.println(user);
}
} finally {
sqlSession.close();
}
}
}
```
### 12.2 MyBatis-Spring核心概念
> 官方文档地址: [http://www.mybatis.org/spring/zh/index.html](http://www.mybatis.org/spring/zh/index.html)
MyBatis-Spring 的核心目标是帮助我们将 MyBatis 无缝地整合到 Spring 容器中,让 Spring 来管理 MyBatis 的组件。
- **`SqlSessionFactoryBean`**:
* 在 Spring 中,我们不再使用 `SqlSessionFactoryBuilder`,而是使用 `SqlSessionFactoryBean` 来创建 `SqlSessionFactory`。
* 它负责读取配置,并创建一个由 Spring 管理的 `SqlSessionFactory` 实例。
* 其最重要的属性是 `dataSource`,用于接收 Spring 管理的数据源。
- **`SqlSessionTemplate`**:
* `SqlSessionTemplate` 是 MyBatis-Spring 的**核心**,它是 `SqlSession` 的一个线程安全实现。
* 它能自动参与到 Spring 的事务管理中,负责 session 的生命周期(获取、提交/回滚、关闭)。
* 在整合后,我们应该**始终使用 `SqlSessionTemplate` 来代替原生的 `DefaultSqlSession`**。
### 12.3 整合实现方式一:SqlSessionTemplate
#### 1. 创建Spring数据层配置文件 (spring-dao.xml)
```xml
```
#### 2. 创建Mapper实现类 (UserMapperImpl.java)
```java
package com.github.subei.mapper;
import com.github.subei.pojo.User;
import org.mybatis.spring.SqlSessionTemplate;
import java.util.List;
public class UserMapperImpl implements UserMapper {
// 我们的所有操作,都使用SqlSessionTemplate来执行
private SqlSessionTemplate sqlSession;
public void setSqlSession(SqlSessionTemplate sqlSession) {
this.sqlSession = sqlSession;
}
public List selectUser() {
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
return mapper.selectUser();
}
}
```
#### 3. 创建Spring主配置文件 (applicationContext.xml)
```xml
```
#### 4. 测试
```java
@Test
public void testSelectUser() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserMapper userMapper = context.getBean("userMapper", UserMapper.class);
for (User user : userMapper.selectUser()){
System.out.println(user);
}
}
```
**结果成功输出!** 此时,原生的 `mybatis-config.xml` 文件中的数据源和事务管理器配置已被 Spring 完全接管,可以被简化。
### 12.4 整合实现方式二:SqlSessionDaoSupport
这是一种更便捷的方式,通过继承 `SqlSessionDaoSupport` 类来简化 Mapper 实现类的编写。
#### 1. 修改Mapper实现类 (UserMapperImpl2.java)
```java
package com.github.subei.mapper;
import com.github.subei.pojo.User;
import org.mybatis.spring.support.SqlSessionDaoSupport;
import java.util.List;
public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper {
public List selectUser() {
// 直接通过 getSqlSession() 获取 SqlSessionTemplate
UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
return mapper.selectUser();
}
}
```
#### 2. 修改Spring配置 (applicationContext.xml)
`SqlSessionDaoSupport` 需要注入 `SqlSessionFactory` 而不是 `SqlSessionTemplate`。
```xml
```
#### 3. 测试 (类似方式一)
```java
@Test
public void testSelectUser2() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserMapper userMapper = context.getBean("userMapper2", UserMapper.class);
// ...
}
```
> **总结**:整合到Spring后,可以完全移除MyBatis配置文件中的数据源和事务配置。除了XML配置,还可以使用注解方式实现整合,这是现代开发中更主流的方式。
---
## 13、声明式事务
### 13.1 回顾事务
**事务** 是一组不可分割的业务操作单元,要么都成功,要么都失败。它用于保证数据的**完整性**和**一致性**。
**事务的ACID原则:**
- **原子性 (Atomicity)**:事务中的所有操作是一个整体,不可分割。
- **一致性 (Consistency)**:事务完成后,数据必须保持业务规则上的一致状态。
- **隔离性 (Isolation)**:多个事务并发执行时,应相互隔离,防止数据损坏。
- **持久性 (Durability)**:事务一旦提交,其结果就是永久性的。
### 13.2 事务失效场景测试
假设我们在一个方法内,先执行一个成功的插入操作,再执行一个失败的删除操作(SQL语法错误)。
**1. 扩展Mapper接口和XML**
```java
// UserMapper.java
public interface UserMapper {
List selectUser();
int addUser(User user);
int deleteUser(int id);
}
```
```xml
insert into user (id,name,pwd) values (#{id},#{name},#{pwd});
deletes from user where id = #{id};
```
**2. 在一个方法中调用**
```java
// UserMapperImpl.java
public List selectUser() {
UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
// 先添加
mapper.addUser(new User(6,"维维","123456"));
// 再删除(这个会失败)
mapper.deleteUser(6);
return mapper.selectUser();
}
```
**测试结果**:程序会因SQL异常而中断,但数据库中**用户添加成功了**!这破坏了数据的一致性,因为我们期望添加和删除是一个整体。
### 13.3 Spring中的事务管理
Spring 提供了两种事务管理方式:
- **编程式事务**:在业务代码中手动控制事务的开启、提交、回滚。侵入性强,不推荐。
- **声明式事务**:通过配置(XML或注解)来管理事务,业务代码无需关心事务逻辑。**这是我们使用的重点**。
#### 配置声明式事务(XML方式)
**1. 配置事务管理器**
首先,需要一个事务管理器 `DataSourceTransactionManager`,并关联我们的数据源。
```xml
```
**2. 配置事务通知 (tx:advice)**
在这里定义事务的规则,比如哪些方法需要事务,以及事务的传播特性。
*需要先引入 `tx` 命名空间及其约束。*
```xml
```
**3. 配置AOP,将事务织入**
使用AOP将事务通知应用到指定的方法上。
```xml
```
#### 再次测试
在配置好声明式事务后,再次运行之前的测试代码。
**结果**:程序依然会报错,但是**数据库中新用户没有被添加**!事务成功回滚,保证了数据的一致性。
> **为什么需要事务?**
>
> - **保证数据一致性**:防止因部分操作失败而导致数据状态混乱。
> - **简化开发**:使用Spring的声明式事务,开发者可以专注于业务逻辑,而无需手动管理复杂的事务代码。
🎉🎉🎉 **Spring核心部分完结撒花!** 🎉🎉🎉
---
## 参考
[【狂神说Java】Spring5最新完整教程IDEA版通俗易懂](https://www.bilibili.com/video/BV1WE411d7Dv)
[Spring学习目录(6天) - subeiLY - 博客园](https://www.cnblogs.com/gh110/p/14382170.html)