0x01 写在前面

很久之前我就学过一点点的 SpringBoot 开发,当时 Spring 一点没学就开始跟着狂神敲项目了(Java 语法都没学,是的你没听错),建空中楼阁和没学过其实也没什么两样,这块对于后续内存马的学习还是很重要的,打的大多数 Java 项目用的也是 Spring 框架,一口吃不成大胖子,先学开发再学攻防嘛

0x02 Spring 简介

Spring 初识

Spring 理念 : 使现有技术更加实用,本身就是一个大杂烩 , 整合现有的框架技术

  • SSH:Struct2 + Spring + Hibernate
  • SSM: SpringMVC + Spring + Mybatis

Spring 的一些特点:

轻量级框架,特点 IoC:控制反转;AOP:切面编程

Spring 组成

Spring 毕竟也是一个框架,所以框架肯定是支持很多功能的,我们要实现功能,或者说要通过框架来完成业务,只需要打配置即可。Spring 框架支持的一些功能如下图所示:

img

简单解释一下这几点东西:

  • 最底下一定是我们的 Core,也就是所谓的 “核”
  • 往上走支持一些编程思想:IOC,AOP
  • 还支持 ORM(也就是处理数据库等语句,可以类比为 Mybatis)
  • 还支持很多 Web,比如 Web Application,Servlet 等

Spring 官网 上有一条这样的学习路线:

img

为什么 Spring 之后是 SpringBoot,然后是 SpringCloud 呢?因为 Spring 的配置非常繁琐,有时候我们不得不配置一些与我们本身的 Web 应用关系不大的东西,于是 SpringBoot 孕育而生

0x03 IOC

IOC 初识

Demo 搭建

新建 IDEA 项目

打开 IDEA:

1
2
3
File
→ New
→ Project

选择:

1
Maven

不要勾:

1
Create from archetype

然后:

1
2
Name:
SpringBasicLearning

例如:

1
2
Name: SpringBasicLearning
Location: D:\JavaProject

点击:

1
Create

此时项目结构大概是:

1
2
3
4
SpringBasicLearning

├─ pom.xml
└─ src

把父工程改成聚合工程

打开根目录:

1
SpringBasicLearning/pom.xml

修改为:

1
2
3
4
5
6
7
8
9
10
11
<project xmlns="http://maven.apache.org/POM/4.0.0">

<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>SpringBasicLearning</artifactId>
<version>1.0-SNAPSHOT</version>

<packaging>pom</packaging>

</project>

注意:

1
<packaging>pom</packaging>

必须有,因为它只是父工程。


删除父工程的 src

因为父工程不写代码,删除:

1
src

最终:

1
2
3
SpringBasicLearning

└─ pom.xml

创建 Module

右键:

1
SpringBasicLearning

选择:

1
2
New
→ Module

如果没有:

1
2
3
File
→ New
→ Module

选择:

1
Maven

点击:

1
Next

填写:

1
2
Name:
IoC_Learn

然后:

1
Finish

此时结构:

1
2
3
4
5
6
7
SpringBasicLearning

├─ pom.xml

└─ IoC_Learn
├─ pom.xml
└─ src

修改父工程 pom

此时 IDEA 通常会自动加:

1
2
3
<modules>
<module>IoC_Learn</module>
</modules>

如果没有,手动加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">

<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>SpringBasicLearning</artifactId>
<version>1.0-SNAPSHOT</version>

<packaging>pom</packaging>

<modules>
<module>IoC_Learn</module>
</modules>

</project>

修改子模块 pom

打开:

1
IoC_Learn/pom.xml

确认有:

1
2
3
4
5
<parent>
<groupId>org.example</groupId>
<artifactId>SpringBasicLearning</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

然后加入 Spring:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.16</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.16</version>
</dependency>

</dependencies>

完整示例:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<parent>
<groupId>org.example</groupId>
<artifactId>SpringBasicLearning</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<modelVersion>4.0.0</modelVersion>

<groupId>com</groupId>
<artifactId>IoC_Learn</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.16</version>
</dependency>

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.16</version>
</dependency>

</dependencies>

</project>

刷新 Maven

右边 Maven 面板,点击:

1
Reload All Maven Projects

等待下载


配置 JDK

检查:

1
2
File
→ Project Structure

或者:

1
Ctrl + Alt + Shift + S

设置:

1
Project SDK

例如:

1
JDK 8

或者:

1
JDK 17

都可以


创建包

展开:

1
2
3
4
IoC_Learn
└─ src
└─ main
└─ java

右键:

1
2
3
java
→ New
→ Package

输入:

1
org.example

创建测试类

创建:

1
2
3
4
5
6
7
8
9
package org.example;

public class Test {

public static void main(String[] args) {
System.out.println("Hello Spring");
}

}

运行成功说明环境搭建完成


最终你应该得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SpringBasicLearning

├─ pom.xml

└─ IoC_Learn

├─ pom.xml

└─ src
└─ main
├─ java
│ └─ org.example
│ └─ Test.java

└─ resources

img

正式开始 IOC 的学习

传统业务实现

在看 IOC 的编程思想之前,先看一看传统的编程思想:

传统的编程思想:Controller 层写接口,去调用 Service 层,Service 层里面有一个 Service 接口,还有一个 ServiceImpl 的实现类,具体的业务是写在 ServiceImpl 里面的

Service 层去调用 Dao 层,也就是我们的实体类,我们的 Dao 层有一个 Dao 的抽象接口,还有一个 DaoImpl 的实现类。

  • 大致的流程就是 ControllerServiceDao

我们用代码来实现一下,看一看为什么要用到 IOC 的这种编程思维

UserDAO.java

1
2
3
4
5
package DAO;  

public interface UserDAO {
public void getUser();
}

**UserDAOImpl.java **UserDAO 的实现类

1
2
3
4
5
6
7
8
9
10
package DAO;  

import DAO.UserDAO;

public class UserDAOImpl implements UserDAO {
@Override
public void getUser() {
System.out.println("输出获取用户数据");
}
}

UserService.java Service 业务层

1
2
3
4
5
package Service;  

public interface UserService {
public void getUser();
}

UserServiceImpl.java Service 业务实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package Service;  

import DAO.UserDAO;
import DAO.UserDAOImpl;

public class UserServiceImpl implements UserService{

private UserDAO userDAO = new UserDAOImpl();

@Override
public void getUser() {
userDAO.getUser();
}
}

我们的 UserService 全程都是在调用 DAO 层的,很有趣

**TestApplication.java **测试启动类

1
2
3
4
5
6
7
8
9
import Service.UserService;
import Service.UserServiceImpl;

public class TestApplication {
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
userService.getUser();
}
}

img

IOC 处理情景

这样子做我们正常的业务是没有问题的,但是现在来了一个客户,他要求我们用 MySQL 获取到 User,这里我们应该把 UserServiceImpl 中的这一句语句加以修改

1
2
3
4
5
private UserDAO userDAO = new UserDAOImpl();

// 修改如下

private UserDAO userDAO = new 对应的 DAOImpl 类

看着也还好对吧,只修改了一个地方,因为我们本质还是去调用 DAO 层的东西了,这是因为我们只有一个 getUser() 方法,后续我们会有更多的方法,比如 getUserId()getUserName()getPasswd() 等等,这时候我们要是一个个换名字,就太麻烦了,这里我们就得采用 IOC 的编程思想

IOC 这一编程思想光看理论有点累,所以我们可以直接用通俗易懂的代码方式来理解

我们在 UserServiceImpl.java 的地方加一个 setter 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package Service;

import DAO.UserDAO;
import DAO.UserDAOImpl;

public class UserServiceImpl implements UserService{

private UserDAO userDAO;

@Override
public void setUserDAO(UserDAO userDAO) {
this.userDAO = userDAO;
}

@Override
public void getUser() {
userDAO.getUser();
}
}

这一个 setter 方法就非常灵性,它灵性于何处呢?我们可以看到在 TestApplication 里面,我们可以这样操作:

1
2
3
4
5
6
7
8
9
10
import Service.UserService;
import Service.UserServiceImpl;

public class TestApplication {
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
userService.setUserDAO(new MysqlUserDAOImpl());
userService.getUser();
}
}

对应的,应该先新建一个 UserServiceImpl 类,去实现 UserDAO 的接口:

1
2
3
4
5
6
7
8
9
package DAO;

public class MysqlDAOImpl implements UserDAO {

@Override
public void getUser() {
System._out_.println("通过 MYSQL 的方式获取 User");
}
}

img

  • 传统方式:你的代码 → 主动创建和管理依赖 → 调用依赖的方法
  • IoC 方式:框架/容器 → 为你创建和管理依赖 → 调用你的代码中的回调或钩子

本质上解决了问题,程序员不用再去管理对象的创建;系统的耦合性大大降低,可以更专注在业务的实现上

这是 IOC(控制反转)的原型,反转(理解):主动权交给了用户;

这就是 Spring 当中的 IOC

IOC 本质

控制反转 IOC(Inversion of Control),是一种设计思想,DI(依赖注入)是实现 IoC 的一种方法,也有人认为 DI 只是 IoC 的另一种说法。没有 IoC 的程序中 , 我们使用面向对象编程 , 对象的创建与对象间的依赖关系完全硬编码在程序中,对象的创建由程序自己控制,控制反转后将对象的创建转移给第三方,个人认为所谓控制反转就是:获得依赖对象的方式反转了

IoC 是 Spring 框架的核心内容,使用多种方式完美的实现了 IoC,可以使用 XML 配置,也可以使用注解,新版本的 Spring 也可以零配置实现 IoC

Spring 容器在初始化时先读取配置文件,根据配置文件或元数据创建与组织对象存入容器中,程序使用时再从 Ioc 容器中取出需要的对象

img

采用 XML 方式配置 Bean 的时候,Bean 的定义信息是和实现分离的,而采用注解的方式可以把两者合为一体,Bean 的定义信息直接以注解的形式定义在实现类中,从而达到了零配置的目的

控制反转是一种通过描述(XML 或注解)并通过第三方去生产或获取特定对象的方式。在 Spring 中实现控制反转的是 IoC 容器,其实现方法是依赖注入(Dependency Injection,DI)

XML 装配

通过读取 xml 文件来读取类,这确实是一个蛮玄乎的地方

这里我们先编写一个 Hello 实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package dao;  

public class Hello {
private String name;

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

public void show(){
System.out.println("Hello,"+ name );
}
}

再写一个 beans.xml,这用于我们 Spring 的装配

1
2
3
4
5
6
7
8
9
10
11
<?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就是java对象 , 由Spring创建和管理-->
<bean id="hello" class="dao.Hello">
<property name="name" value="Spring"/>
</bean>
</beans>

这里的 id 和 class 其实和 html 里面蛮像的,id 必然是只能有一个,class 就是对应我们要去装配的类,就和 Servlet 项目里面的 class 调用是一样的

property 就是给对象中的属性设置一个值,用于进行依赖注入(DI),这里是典型的 Setter 注入:

Spring 会根据这个名字自动拼接出 setter 方法名:set + 首字母大写 → setName

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
import dao.Hello;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class test {
@Test
public void test() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Hello hello = (Hello) context.getBean("hello");
hello.show();
}
}

为便于测试,pox.xml 中要添加 JUnit 测试框架的依赖

1
2
3
4
5
6
7
<!-- JUnit 4 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>

img

装配模式的思考

  • Hello 对象是谁创建的 ? hello 对象是由 Spring 创建的
  • Hello 对象的属性是怎么设置的 ? hello 对象的属性是由 Spring 容器设置的

这个过程就叫控制反转:

  • 控制 : 谁来控制对象的创建 , 传统应用程序的对象是由程序本身控制创建的 , 使用 Spring 后 , 对象是由 Spring 来创建的
  • 反转 : 程序本身不创建对象 , 而变成被动的接收对象

IOC 是一种编程思想,由主动的编程变成被动的接收

这是我们通过 .xml 文件获取类,并装配;如果我们根据不同的业务进行装配呢?就和之前的场景一致,有的业务需要我们从 MySQL 读取数据,有的业务需要我们从 Oracle 读取数据,我们该如何实现呢?

IOC 装配思想

回到我们之前讲 IOC 的案例里面,我们新建一个 beans.xml,内容如下

1
2
3
4
5
6
7
8
9
10
11
12
<?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="mysqlImpl" class="DAO.MysqlDAOImpl"/>
<bean id="oracleImpl" class="DAO.OracleUserDaoImpl"/>
<bean id="UserServiceImpl" class="Service.UserServiceImpl">
<!--ref引用spring中已经创建很好的对象-->
<!--value是一个具体的值,基本数据类型-->
<property name="userDAO" ref="mysqlImpl"/>
</bean>
</beans>

对应的启动器:

img

假若这时候需要 OracleImpl 对象,则只需在 xml 配置文件中设置 ref="oracleImpl" 即可,无需去改动代码

总结:

  • 所有的类都要装配的 beans.xml 里面;
  • 所有的 bean 都要通过容器去取;
  • 容器里面取得的 bean,拿出来就是一个对象,用对象调用方法即可;

**所谓的 IOC,一句话搞定:对象由 Spring 来创建,管理,装配 **

IOC 对象创建

IOC 主要有两种创建的方式,其他都不行的:

方法一:通过无参构造方法来创建

User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class User {
private String name;

public User() {
System.out.println("user无参构造方法");
}

public String getName() {
return name;
}

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

public void show(){
System.out.println("name="+ name );
}
}

beans.xml

1
2
3
4
5
6
7
8
9
<?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="user" class="com.example.pojo.User">
<property name="name" value="Drunkbaby"/>
</bean>
</beans>

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
import dao_.User;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TestApplication {
@Test
public void TestApplication() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) context.getBean("user");
user.show();
}
}

img

方法二:通过有参构造方法来创建

User 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package dao_;

public class User {
private String name;

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

public String getName() {
return name;
}

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

public void show(){
System._out_.println("name="+ name );
}
}

beans.xml 有三种方式编写

  • 下标赋值
1
2
3
4
<bean id="user" class="dao_.User">
<!-- index指构造方法 , 下标从0开始 -->
<constructor-arg index="0" value="G3ng4r"/>
</bean>
  • 类型赋值(不建议使用)
1
2
3
<bean id="user" class="dao_.User">
<constructor-arg type="java.lang.String" value="G3ng4r"/>
</bean>
  • 参数名赋值
1
2
3
4
<bean id="user" class="dao_.User">
<!-- name指参数名 -->
<constructor-arg name="name" value="G3ng4r"/>
</bean>

结论:在配置文件加载的时候。其中管理的对象都已经初始化了(并且是单例模式,取到的对象是全局唯一)!

1
2
3
User user = (User) context.getBean("user");
User user2 = (User) context.getBean("user");
system.out.println(user == user2) //结果为true

0x04 Spring 配置

在讲这个知识点之前,我们最好先再次理解一下封装的思维,以及为什么要用 Spring 来配置

Spring 经常被吐槽写配置写到吐,这不是没有原因的,因为确实是要写配置写到吐

我们前文说到 Spring 当中 有一个 bean.xml 叫做 applicationContext.xml,它是用来统领所有配置的,我们举个情景来说明一下。

有三位程序员,他们分别是张三、李四、王五

他们分别写了代码,把程序打包到了 zhangsan.xmllisi.xmlwangwu.xml

这时候,我们的 applicationContext.xml 来统领所有的配置,这样就非常符合我们的封装思想,很舒服

  • 现在我们再回来讲一讲 Spring 的一些基础配置参数与其他的东西

别名设置

alias 设置别名 , 为 bean 设置别名 , 并且可以设置多个别名

1
2
<!--设置别名:在获取Bean的时候可以使用别名获取-->
<alias name="user" alias="userNew"/>

这样在我们要调用的时候可以调用 name,也可以调用别名

Bean 配置

  • id:bean 的唯一标识符,也就是相当于我们学的对象名
  • class:bean 对象所对应的会限定名:包名 + 类型
  • name:也是别名,而且 name 可以同时取多个别名
1
2
3
<bean id="user" class="com.example.User" name="u1 u2,u3;u4">
<property name="name" value="G3ng4r"/>
</bean>

import

import 一般用于团队开发使用,它可以将多个配置文件,导入合并为一个

  • 张三(beans.xm1)
  • 李四(beans2.xm1)
  • 王五(beans3.xm1)
  • applicationContext.xml
1
<import resource="beans.xm1"/>  <import resource="beans2.xml"/>  <import resource="beans3.xm1"/>

使用的时候,直接使用总的配置就可以了

有重名的情况出现时,按照在总的 xml 中的导入顺序来进行创建,后导入的会重写先导入的,最终实例化的对象会是后导入 xml 中的那个

0x05 DI 依赖注入

构造注入

前文已经说过,其实就是对应的 IOC 业务情景

Set 方法注入

依赖注入:本质上是 Set 注入

  • 依赖:bean 对象的创建依赖于容器
  • 注入:bean 对象中的所有属性由容器来注入

pojo 实体类

Address.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package pojo;

public class Address {
private String address;

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}
}

Student:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
package pojo;

import java.util.*;

public class Student {
private String name;
private Address address;
private String[] books;
private List<String> hobbies;
private Map<String,String> card;
private Set<String> games;
private String wife;
private Properties info;

public String getName() {
return name;
}

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

public Address getAddress() {
return address;
}

public void setAddress(Address address) {
this.address = address;
}

public String[] getBooks() {
return books;
}

public void setBooks(String[] books) {
this.books = books;
}

public List<String> getHobbies() {
return hobbies;
}

public void setHobbies(List<String> hobbies) {
this.hobbies = hobbies;
}

public Map<String, String> getCard() {
return card;
}

public void setCard(Map<String, String> card) {
this.card = card;
}

public Set<String> getGames() {
return games;
}

public void setGames(Set<String> games) {
this.games = games;
}

public String getWife() {
return wife;
}

public void setWife(String wife) {
this.wife = wife;
}

public Properties getInfo() {
return info;
}

public void setInfo(Properties info) {
this.info = info;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", address=" + address +
", books=" + Arrays._toString_(books) +
", hobbies=" + hobbies +
", card=" + card +
", games=" + games +
", wife='" + wife + '\'' +
", info=" + info +
'}';
}
}

xml 注入文件

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="student" class="pojo.Student">
<property name="name" value="Drunkbaby"/>
</bean>

</beans>

测试启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pojo.Student;

public class TestApplication {
@Test
public void TestApplication() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Student student = (Student) context.getBean("student");
System._out_.println(student.getName());
}
}

img

官方文档上支持非常多的注入方式,如图:

img

Bean 注入

tips:这里的值是一个引用,Spring 会在容器中找到 id 为 addr 的 Bean,并将其作为参数传递给 StudentsetAddress(Address address) 方法

1
2
3
4
5
6
7
<<bean id="addr" class="pojo.Address">  
<property name="address" value="China"/>
</bean>

<bean id="student" class="pojo.Student">
<property name="address" ref="addr"/>
</bean>

数组注入

1
2
3
4
5
6
7
8
<property name="books">  
<array>
<value>西游记</value>
<value>三国演义</value>
<value>水浒传</value>
<value>红楼梦</value>
</array>
</property>

List 注入

1
2
3
4
5
6
7
8
<property name="hobbies">  
<list>
<value></value>
<value></value>
<value>rap</value>
<value>篮球</value>
</list>
</property>

Map 注入

1
2
3
4
5
6
7
<!--map键值对注入 -->
<property name="card">
<map>
<entry key="username" value="root" />
<entry key="password" value="12345" />
</map>
</property>

set 注入

1
2
3
4
5
6
7
8
<!--set(可去重)注入 -->
<property name="games">
<set>
<value>anno</value>
<value>soyorin</value>
<value>tomorin</value>
</set>
</property>

空指针 null 注入

1
2
3
4
<!--空指针null注入 -->
<property name="wife">
<null></null>
</property>

properties 常量注入

1
2
3
4
5
6
7
<!--properties常量注入 -->
<property name="info">
<props>
<prop key="id">114514</prop>
<prop key="name">G3ng4r</prop>
</props>
</property>

修改 Test 类如下

TestApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.junit.Test;  
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pojo.Student;


public class TestApplication {
@Test
public void TestApplication() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
//在执行getBean的时候, user已经创建好了 , 通过无参构造
Student student = (Student) context.getBean("student");
System.out.println(student);
}
}

拓展方式注入

在官方文档中,它们是这两个注入方式,也被称之为 c 命令与 p 命名空间注入

img

其实这个地方,P 就代表着 Properties,c 就代表着 Constructor

所以这两个注入方式也不是什么很稀奇的事情,只是减轻了我们写配置文件的繁琐程度

环境准备

User.java

tips:这里没有有参构造器!后续我们就可以看到为什么需要加上有参构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package pojo;

public class User {
private String name;
private int age;

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

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

}

p 命名空间注入

需要在头文件中加入约束文件。也不是说完全的约束文件吧,应该算是一个命名空间,就和 mybatis 那些一样的

p 注入代码:

1
2
3
4
导入约束 : xmlns:p="http://www.springframework.org/schema/p"

<!--P(属性: properties)命名空间 , 属性依然要设置set方法-->
<bean id="user" class="pojo.User" p:name="G3ng4r" p:age="20" />

编写一下 Test 类

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pojo.User;

public class TestApplication {
@Test
public void TestApplication() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = (User) context.getBean("user");
System._out_.println(user);
}
}

img

c 命名空间注入

也需要在头文件中加入约束文件

1
2
3
4
导入约束 : 
xmlns:c="http://www.springframework.org/schema/c"
<!--C(构造: Constructor)命名空间 , 属性依然要设置set方法-->
<bean id="user" class="pojo.User" c:name="G3ng4r" c:age="20"/>

爆红了,来看看官方文档对于 C 命名空间的说明:

img

解决:把有参构造器加上,这里也能知道,c 就是所谓的构造器注入

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
package pojo;

public class User {
private String name;
private int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}

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

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

}

img

Bean 的作用域

在 Spring 中,那些组成应用程序的主体及由 Spring IoC 容器所管理的对象,被称之为 bean

简单地讲,bean 就是**由 IoC 容器初始化、装配及管理的对象 **

img

几种作用域中,request、session 作用域仅在基于 Web 的应用中使用(不必关心你所采用的是什么 Web 应用框架),只能用在基于 Web 的 Spring ApplicationContext 环境

单例模式

Spring 默认机制

单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案

采用单例模式动机

对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或 ID(序号)生成器。如在 Windows 中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口,如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要
如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。一个更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机

单例模式优缺点

  • 优点
    一、实例控制
    单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
    二、灵活性
    因为类控制了实例化过程,所以类可以灵活更改实例化过程
  • 缺点
    一、开销
    虽然数量很少,但如果每次对象请求引用时都要检查是否存在类的实例,将仍然需要一些开销。可以通过使用静态初始化解决此问题。
    二、可能的开发混淆
    使用单例对象(尤其在类库中定义的对象)时,开发人员必须记住自己不能使用 new 关键字实例化对象。因为可能无法访问库源代码,因此应用程序开发人员可能会意外发现自己无法直接实例化此类。
    三、对象生存期

不能解决删除单个对象的问题。在提供内存管理的语言中(例如基于.NET Framework 的语言),只有单例类能够导致实例被取消分配,因为它包含对该实例的私有引用。在某些语言中(如 C++),其他类可以删除对象实例,但这样会导致单例类中出现悬浮引用

单例模式的简单理解

  • 单例模式 只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等
  • 单例的缺点 就是不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态
    用单例模式,就是在适用其优点的状态下使用

默认模式

1
<bean id="user" class="pojo.User"" c:name="G3ng4r" c:age="20" scope="singleton"></bean>

原型模式(Prototype)

每次从容器中 get 的时候,都产生一个新对象

1
<bean id="user" class="pojo.User" c:name="G3ng4r" c:age="20" scope="prototype"></bean>

其余的 request、session、application 这些只能在 Web 开放中使用

这些的部分我们会在内存马里面稍微提到,但是在那里面是 SpringBoot 的配置,所以会简单很多

0x06 Bean 自动装配

自动装配说明

由于在手动配置 xml 过程中,常常发生字母缺漏和大小写等错误,而无法对其进行检查,使得开发效率降低。采用自动装配将避免这些错误,并且使配置简单化。

  • 自动装配是使用 spring 满足 bean 依赖的一种方法
  • spring 会在应用上下文中为某个 bean 寻找其依赖的 bean。

Spring 中 bean 有三种装配机制,分别是:

  • 在 xml 中显式配置;
  • 在 Java 中显式配置;
  • 隐式的 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package pojo;

public class People {

private String name;
private Cat cat;
private Dog dog;

@Override
public String toString() {
return "People{" +
"cat=" + cat +
", rabbit=" + dog +
", name='" + name + '\'' +
'}';
}

public Cat getCat() {
return cat;
}

public void setCat(Cat cat) {
this.cat = cat;
}

public Dog getDog() {
return dog;
}

public void setDog(Dog dog) {
this.dog = dog;
}

public String getName() {
return name;
}

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

编写 Spring 配置文件

beans.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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="cat" class="pojo.Cat"/>
<bean id="dog" class="pojo.Dog"/>

<bean id="peple" class="pojo.People">
<property name="name" value="G3ng4r"/>
<property name="cat" ref="cat"/>
<property name="dog" ref="dog"/>
</bean>
</beans>

测试启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pojo.People;

public class TestApplication {
@Test
public void TestApplication() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
People people = (People) context.getBean("people");
people.getCat().shout();
people.getDog().shout();
System._out_.println(people);
}
}

img

byName 自动装配

比较简单,修改 beans.xml 这一配置文件即可

去找 beanId ,这个 Id 必须要是 setter 方法里有的,比如上文代码中,我们只可以 byNamebeanId 是 cat 和 dog

img

错误的实例:此时我们的 beanId 并不存在 dog123,但是存在 cat,所以 cat 的 shout() 方法能够被正常调用

img

正确配置:

1
2
3
4
5
6
7
8
9
10
11
12
<?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="cat" class="pojo.Cat"/>
<bean id="dog" class="pojo.Dog"/>

<bean id="people" class="pojo.People" autowire="byName">
<property name="name" value="G3ng4r"/>
</bean>
</beans>

img

byType 自动装配

byType 的自动装配,去找的是 class 属性,是否是类当中的,也就是寻找 Bean

1
2
3
4
5
6
7
8
9
10
11
12
<?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 class="pojo.Cat"/>
<bean class="pojo.Dog"/>

<bean id="people" class="pojo.People" autowire="byType">
<property name="name" value="G3ng4r"/>
</bean>
</beans>

img

要求 class 相同即可

使用注解自动装配

jdk1.5 开始支持注解,Spring2.5 开始全面支持注解

准备工作:利用注解的方式注入属性

在 spring 配置文件中引入 context 文件头,这里可以通过添加这一行,代表支持注解的语句,会自动引入文件头

1
<context:annotation-config/>

对于注解进行自动装配,对于我们最耳熟能详的,当属 @AutoWired 这个注解

@AutoWired

@AutoWired 注解可以替代 setter 方法,这里我们可以在 People.java 这个文件夹下面动刀

People.java:

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
package pojo;

import org.springframework.beans.factory.annotation.Autowired;

public class People {

private String name;
@Autowired
private Cat cat;
@Autowired
private Dog dog;

@Override
public String toString() {
return "People{" +
"cat=" + cat +
", rabbit=" + dog +
", name='" + name + '\'' +
'}';
}



public String getName() {
return name;
}

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

beans.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>
<bean class="pojo.Cat"/>
<bean class="pojo.Dog"/>

<bean id="people" class="pojo.People" autowire="byType">
<property name="name" value="G3ng4r"/>
</bean>
</beans>

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pojo.People;

public class TestApplication {
@Test
public void TestApplication() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
People people = (People) context.getBean("people");
System._out_.println(people);
}
}

img

Tips1:

@Nullable 字段标记了这个注解,说明该字段可以为空:

1
2
3
public People(@Nullable String name) {  
this.name = name;
}

Tips2:

required 字段为 false 时,对象可以为 null;为 true 时,对象必须存对象,不能为 null;默认为 true

1
2
@Autowired(required = false)
private Cat cat;

@Qualifier

是用来指定 ID 的,和 byName 很像

@Autowired 不能唯一装配时,需要 @Autowired+@Qualifier

如果 @Autowired 自动装配环境比较复杂。自动装配无法通过一个注解完成的时候,可以使用 @Qualifier(value = “xxx”) 去配合 @Autowired 使用,指定一个唯一的 id 对象

配置文件修改内容,保证类型存在对象。且名字不为类的默认名字

1
2
<bean id="cat1" class="pojo.Cat"/>
<bean id="dog1" class="pojo.Dog"/>

在属性上添加 Qualifier 注解:

1
2
3
4
5
6
@Autowired
@Qualifier(value = "cat1")
private Cat cat;
@Autowired
@Qualifier(value = "dog1")
private Dog dog;

@Resource

非常灵活的一种自动注入,可以理解为 byName + byType

  • @Resource 如有指定的 name 属性,先按该属性进行 byName 方式查找装配;
  • 其次再进行默认的 byName 方式进行装配;
  • 如果以上都不成功,则按 byType 的方式自动装配。
  • 都不成功,则报异常

配置文件修改内容,保证类型存在对象。且名字不为类的默认名字

1
2
<bean id="cat1" class="pojo.Cat"/>
<bean id="dog1" class="pojo.Dog"/>

修改 People.java,使用 @Resource 注解

1
2
3
4
5
private String name;
@Resource(name = "cat1")
private Cat cat;
@Resource(name = "dog1")
private Dog dog;

小结

@Autowired@Resource 异同:

  • @Autowired@Resource 都可以用来装配 bean。都可以写在字段上,或写在 set 方法上
  • @Autowired 通过 byType 的方式实现,而且必须要求这个对象存在!(常用)
  • @Resource 默认通过 byname 的方式实现,如果找不到名字,则通过 byType 实现!如果两个都找不到的情况下,就报错!(常用)
  • 它们的作用相同都是用注解方式注入对象,但执行顺序不同。@Autowired 先 byType,@Resource 先 byName

0x07 注解开发

注解说明

在 Spring4 之后,想要使用注解形式,必须得要引入 aop 的包,这个在 Spring-webmvc 的大包里面基本也是自带的。

我们之前都是使用 bean 的标签进行 bean 注入,但是实际开发中,我们一般都会使用注解

配置扫描哪些包下的注解:

1
2
<!--指定注解扫描包-->
<context:component-scan base-package="pojo"/>

在指定包下编写类,增加注解:

1
2
3
4
5
@Component("user")
// 相当于配置文件中 <bean id="user" class="当前注解的类"/>
public class User {
public String name = "G3ng4r";
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import pojo.User;

public class TestApplication {
@Test
public void TestApplication() {
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
User user = context.getBean("user", User.class);
System._out_.println(user);
}
}

img

属性注入

可以不用提供 setter 方法,直接在直接名上添加 @Value(“值”)

1
2
3
4
5
6
public class User {
@Value("G3ng4r")
private String name;
@Value("20")
private int age;
}

如果提供了 set 方法,在 set 方法上添加 @Value("xxx");

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
package pojo;

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

@Component
public class User {
private String name;
private int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}

public User() {
}

@Value("G3ng4r")
public void setName(String name) {
this.name = name;
}

@Value("20")
public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

}

img

这个 @Value 的注解在 Spring-mvc 的项目会经常用到,而里面的这个值可以写在 application.properties 中,到时候就可以直接调用

衍生注解

这些注解就是替代了在配置文件当中配置步骤而已,更加的方便快捷

@Component 三个衍生注解:

为了更好的进行分层,Spring 可以使用其它三个注解,功能一样,目前使用哪一个功能都一样。

  • @Controller:web 层
  • @Service:service 层
  • @Repository:dao 层

写上这些注解,就相当于将这个类交给 Spring 管理装配了!学习 SpringBoot 的时候这种思想就更为明显。

自动装配注解

@Autowired:默认是 byType 方式,如果匹配不上,就会 byName

@Nullable:字段标记了这个注解,说明该字段可以为空

@Resource:默认是 byName 方式,如果匹配不上,就会 byType

作用域

@scope

  • singleton:默认的,Spring 会采用单例模式创建这个对象。关闭工厂 ,所有的对象都会销毁。
  • prototype:多例模式。关闭工厂 ,所有的对象不会销毁。内部的垃圾回收机制会回收

小结

xml 与注解:

  • xml 更加万能,维护简单,适用于任何场合
  • 注解不是自己的类使用不了,维护复杂,开发简单方便

最佳实践:

  • xml 用来管理 Bean
  • 注解只用来完成属性的注入
  • 可以不用扫描,扫描是为了类上的注解,但是要开启注解支持 <context:annotation-config/>

0x08 Java-Spring 配置

这个到了 SpringBoot 里面可以说是主流的选择了,也就是我们平常写项目时的 config 文件夹,因为 Spring 本身写 XML 太累了,繁琐至极,所以用 Java 的方式写 config,可以使得我们代码的可读性更高

User.java

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
package pojo;

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

// 这里这个注解的意思,就是说明这个类被Spring接管了,注册到了容器中
@Component
public class User {
private String name;
private int age;

public User(String name, int age) {
this.name = name;
this.age = age;
}

public User() {
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

@Value("G3ng4r")
public void setName(String name) {
this.name = name;
}

@Value("20")
public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

}

MyConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import pojo.User;

@Configuration
@ComponentScan("pojo")
public class Myconfig {

@Bean
public User getUser() {
return new User();
}
}

测试启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import config.Myconfig;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import pojo.User;

public class TestApplication {
@Test
public void TestApplication() {
ApplicationContext context = new AnnotationConfigApplicationContext(Myconfig.class);
User user = (User) context.getBean("getUser");
System._out_.println(user.getName());
System._out_.println(user.getAge());
}
}

img

0x09 代理模式与 AOP

先说说什么是代理模式,要说代理模式,得从代理说起。下面一张图中的中介,就是我们所说的代理

img

静态代理

简单理解

  • 以租客找中介向房东租房子为例

想要实现租客找中介租房东,在 Java 中就需要 4 个文件,分别是房源、房东、中介、租客,其中房源应该是接口,其余三项为类。以房源为接口,这与 Java 编程的设计思想有关

  • Rent.java:这是一个接口,可以抽象的理解为房源,作为房源,它有一个方法 rent()租房

Rent.java

1
2
3
4
5
6
package Proxy;

// 租房的接口
public interface Rent {
public void rent();
}
  • Host.java:这是一个类,这个类就是房东,作为房东,他需要实现 Rent.java 这一个接口,并且要实现接口的 rent() 方法
1
2
3
4
5
6
7
8
package Proxy;

public class Host {

public void rent() {
System._out_.println("房东要出租房子");
}
}
  • Client.java:这是一个启动类,这个类其实就是租客,租客的想法也很简单,就是找到中介,然后租房(为什么不直接找房东呢?因为房东通常不想管那么多事,而且房源基本被中介垄断)

因为租客是要去找中介看房的,而不是去找房东看房的,所以我们这里先把 Proxy.java 实现一下,也就是把中介相关的功能先实现一下

Proxy.java:这是一个类,这个类是中介,也就是代理,他需要有房东的房源,然而我们通常不会继承房东,而会将房东作为一个私有的属性 host,我们通过 host.rent() 来实现租房的方法

Proxy.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package Proxy;

public class Proxy {

private Host host;

public Proxy() {}

public Proxy(Host host) {
this.host = host;
}

public void rent() {
host.rent();
}

}

这就是基本的看房流程

img

但是事实上,租房的过程肯定不止于此,中介还要收中介费,有一些行为是中介可以做的,而房东不能做的,比如看房,收中介费等等。所以我们要在 Proxy.java 当中实现这些功能

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
package Proxy;

public class Proxy {

private Host host;

public Proxy() {}

public Proxy(Host host) {
this.host = host;
}

public void rent() {
host.rent();
}

// 看房
public void seeHouse(){
System._out_.println("中介带你看房");
}

// 收中介费
public void fare(){
System._out_.println("收中介费");
}

// 签租赁合同
public void contract(){
System._out_.println("签租赁合同");
}
}

改进 Proxy.java

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 Proxy;

public class Proxy {

private Host host;

public Proxy() {}

public Proxy(Host host) {
this.host = host;
}

public void rent() {
host.rent();
seeHouse();
contract();
fare();
}

// 看房
public void seeHouse(){
System._out_.println("中介带你看房");
}

// 收中介费
public void fare(){
System._out_.println("收中介费");
}

// 签租赁合同
public void contract(){
System._out_.println("签租赁合同");
}
}

img

优点:

  • 可以使得我们的真实角色更加纯粹 . 不再去关注一些公共的事情 .
  • 公共的业务由代理来完成 . 实现了业务的分工 ,
  • 公共业务发生扩展时变得更加集中和方便 .

缺点 :

  • 一个真是类对应一个代理角色,代码量翻倍,开发效率降低 .

我们想要静态代理的好处,又不想要静态代理的缺点,所以 , 就有了动态代理

深入理解

深入到实际业务当中,比如我们平常做的最多的 CRUD(增删改查

  • UserService.java,这是一个接口,我们定义四个抽象方法
1
2
3
4
5
6
7
8
package Proxy;

public interface UserService {
public void add();
public void delete();
public void update();
public void query();
}

我们需要一个真实对象来完成这些增删改查操作
UserServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package Proxy;

public class UserServiceImpl implements UserService {

@Override
public void add() {
System._out_.println("增加了一个用户");
}

@Override
public void delete() {
System._out_.println("删除了一个用户");
}

@Override
public void update() {
System._out_.println("更新了一个用户");
}

@Override
public void query() {
System._out_.println("查询了一个用户");
}
}

需求来了,现在我们需要增加一个日志功能,怎么实现!

  • 思路 1 :在实现类上增加代码,但是过于麻烦
  • 思路 2:使用代理来做,能够不改变原来的业务情况下,实现此功能就是最好的了

通过增加一个代理类来处理日志

UserServiceProxy.java

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
package Proxy;

public class UserServiceProxy implements UserService {

private UserServiceImpl userService;

public void setUserService(UserServiceImpl userService) {
this.userService = userService;
}

@Override
public void add() {
log("add");
userService.add();
}

@Override
public void delete() {
log("delete");
userService.delete();
}

@Override
public void update() {
log("update");
userService.update();
}

@Override
public void query() {
log("query");
userService.query();
}

public void log(String msg) {
System._out_.println("[Debug]使用了 \" + msg +\"方法");
}
}

修改启动器 Client.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package Proxy;

public class Client {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl();
UserServiceProxy proxy = new UserServiceProxy();
proxy.setUserService(userService);
proxy.add();
proxy.delete();
proxy.update();
proxy.query();
}
}

如此一来,增加业务点的日志便成功了

img

动态代理

  • 前文我们说到静态代理的问题,还记得吗?

每多一个房东就需要多一个中介,这显然不符合生活认知(对于租客来说,如果是用静态代理模式,每当想要换一个房东,那就必须要再换一个中介,在开发中,如果有多个中介代码量就更大了)

动态代理的出现就是为了解决上面静态代理的缺点

基础知识

  • 动态代理的角色和静态代理的一样。需要一个实体类,一个代理类,一个启动器
  • 动态代理的代理类是动态生成的,静态代理的代理类是我们提前写好的

JDK 的动态代理需要了解两个类

核心 : InvocationHandler 调用处理程序类和 Proxy 代理类

InvocationHandler:调用处理程序

1
public interface InvocationHandler

InvocationHandler 是由代理实例的调用处理程序实现的接口

每个代理实例都有一个关联的调用处理程序

1
2
3
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

当在代理实例上调用方法的时候,方法调用将被编码并分派到其调用处理程序的 invoke() 方法

注意:invoke 方法在动态代理当中会自动执行

参数

  • proxy -调用该方法的代理实例
  • method -所述方法对应于调用代理实例上的接口方法的实例。方法对象的声明类将是该方法声明的接口,它可以是代理类继承该方法的代理接口的超级接口
  • args -包含的方法调用传递代理实例的参数值的对象的阵列,如果接口方法没有参数则为 null。原始类型的参数包含在适当的原始包装器类的实例中,例如 java.lang.Integerjava.lang.Boolean

Proxy : 代理

1
public class Proxy extends Object implements Serializable

Proxy 提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类

动态代理类 (以下简称为代理类 )是一个实现在类创建时在运行时指定的接口列表的类,具有如下所述的行为:

  • 代理接口是由代理类实现的接口
  • 代理实例是代理类的一个实例
1
public static Object newProxyInstance(ClassLoader loader, 类<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException

返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序

参数

  • loader – 类加载器来定义代理类
  • interfaces – 代理类实现的接口列表
  • h – 调度方法调用的调用处理函数

代码实现

要写动态代理的代码,需要抓牢两个要点

  • 代理的是接口,而不是单个用户
  • 代理类不是静态定死的,而是动态生成的

首先是我们的接口类
UserService.java

1
2
3
4
5
6
7
8
package Proxy;

public interface UserService {
public void add();
public void delete();
public void update();
public void query();
}

接着,我们需要用实体类去实现这个抽象类

UserServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package Proxy;

public class UserServiceImpl implements UserService {

@Override
public void add() {
System._out_.println("增加了一个用户");
}

@Override
public void delete() {
System._out_.println("删除了一个用户");
}

@Override
public void update() {
System._out_.println("更新了一个用户");
}

@Override
public void query() {
System._out_.println("查询了一个用户");
}
}

接着是动态代理的实现类

UserproxyInvocationHandler.java

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
package Proxy;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class UserproxyInvocationHandler implements InvocationHandler {

private UserService userService;

public void setUserService(UserService userService) {
this.userService = userService;
}

public Object getProxy() {
Object obj = Proxy._newProxyInstance_(this.getClass().getClassLoader(), userService.getClass().getInterfaces(), this);
return obj;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log(method);
Object obj = method.invoke(userService, args);
return obj;
}

public void log(Method method){
System._out_.println("[Info] " + method.getName() + "方法被调用");
}
}
  • 最后编写我们的 Client,也就是启动器

Client.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package Proxy;

public class Client {
public static void main(String[] args) {
// 真实角色
UserServiceImpl userServiceImpl = new UserServiceImpl();
// 代理角色
UserproxyInvocationHandler handler = new UserproxyInvocationHandler();
// 设置代理对象
handler.setUserService(userServiceImpl);
// 动态生成代理类
UserService proxy = (UserService) handler.getProxy();
proxy.add();
proxy.delete();
proxy.update();
proxy.query();
}
}

img

上述我们的动态代理便完成了

0x10 mybatis 实际开发

基础开发

导入依赖

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
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.1.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>compile</scope>
</dependency>
</dependencies>

实体类 User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.g3ng4r.pojo;

public class User {

private int id;
private String name;
private String pwd;

@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", pwd='" + pwd + '\'' +
'}';
}
}

编写核心配置文件 mybatis-config.xml

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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

<typeAliases>
<package name="com.g3ng4r.pojo"/>
</typeAliases>

<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false&amp;useUnicode=true&amp;characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>

<mappers>
<package name="com.g3ng4r.mapper"/>
</mappers>
</configuration>

编写接口 UserMapper

1
2
3
4
5
6
7
8
9
package com.g3ng4r.mapper;

import com.g3ng4r.pojo.User;

import java.util.List;

public interface UserMapper {
public List<User> selectUser();
}

编写对应的 xml 文件 UserMapper.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.g3ng4r.mapper.UserMapper">

<select id="selectUser" resultType="User">
select * from test.user
</select>

</mapper>

测试类 MybatisTest

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.g3ng4r;

import com.g3ng4r.mapper.UserMapper;
import com.g3ng4r.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class MybatisTest {
@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();

UserMapper mapper = sqlSession.getMapper(UserMapper.class);

List<User> userList = mapper.selectUser();
for(User user : userList){
System._out_.println(user);
}

sqlSession.close();
}

}

img

Mybatis-Spring

上文是一个正常的 Mybatis 程序的应用,接下来,我们要把它与 Spring 做一个整合,充分发挥出 Spring 的特点

在整合之前,不免的,要唠点基础知识

基础知识

在开始使用 MyBatis-Spring 之前,你需要先熟悉 Spring 和 MyBatis 这两个框架和有关它们的术语。这很重要

MyBatis-Spring 需要以下版本:

MyBatis-Spring
MyBatis
Spring 框架
Spring Batch
Java
2.0
3.5+
5.0+
4.0+
Java 8+
1.3
3.4+
3.2.2+
2.1+
Java 6+

如果使用 Maven 作为构建工具,仅需要在 pom.xml 中加入以下代码即可:

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.2</version>
</dependency>

要和 Spring 一起使用 MyBatis,需要在 Spring 应用上下文中定义至少两样东西:一个 SqlSessionFactory 和至少一个数据映射器类

关于 SqlSessionFactory

在 MyBatis-Spring 中,可使用 SqlSessionFactoryBean 来创建 SqlSessionFactory。要配置这个工厂 bean,只需要把下面代码放在 Spring 的 XML 配置文件中:

1
2
3
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>

注意:SqlSessionFactory 需要一个 DataSource(数据源)。这可以是任意的 DataSource,只需要和配置其它 Spring 数据库连接一样配置它就可以了

在基础的 MyBatis 用法中,是通过 S qlSessionFactoryBuilder 来创建 SqlSessionFactory 的。而在 MyBatis-Spring 中,则使用 SqlSessionFactoryBean 来创建

在 MyBatis 中,你可以使用 SqlSessionFactory 来创建 SqlSession。一旦你获得一个 session 之后,你可以使用它来执行映射了的语句,提交或回滚连接,最后,当不再需要它的时候,你可以关闭 session

SqlSessionFactory 有一个唯一的必要属性:用于 JDBC 的 DataSource。这可以是任意的 DataSource 对象,它的配置方法和其它 Spring 数据库连接是一样的

一个常用的属性是 configLocation,它用来指定 MyBatis 的 XML 配置文件路径。它在需要修改 MyBatis 的基础配置非常有用。通常,基础配置指的是 <settings><typeAliases> 元素

关于 SqlSessionTemplate

SqlSessionTemplate 是 MyBatis-Spring 的核心。作为 SqlSession 的一个实现,这意味着可以使用它无缝代替你代码中已经在使用的 SqlSession

模板可以参与到 Spring 的事务管理中,并且由于其是线程安全的,可以供多个映射器类使用,你应该总是用 SqlSessionTemplate 来替换 MyBatis 默认的 DefaultSqlSession 实现。在同一应用程序中的不同类之间混杂使用可能会引起数据一致性的问题

可以使用 SqlSessionFactory 作为构造方法的参数来创建 SqlSessionTemplate 对象

1
2
3
<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSessionFactory" />
</bean>

现在,这个 bean 就可以直接注入到你的 DAO bean 中了。你需要在你的 bean 中添加一个 SqlSession 属性,就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
public class UserDaoImpl implements UserDao {

private SqlSession sqlSession;

public void setSqlSession(SqlSession sqlSession) {
this.sqlSession = sqlSession;
}

public User getUser(String userId) {
return sqlSession.getMapper...;
}
}

按下面这样,注入 SqlSessionTemplate

1
2
3
<bean id="userDao" class="org.mybatis.spring.sample.dao.UserDaoImpl">
<property name="sqlSession" ref="sqlSession" />
</bean>

上述都是官方文档的做法,可以根据我们自己的业务需求进行整合

0x11 声明式事务

回顾事务

  • 事务在项目开发过程非常重要,涉及到数据的一致性的问题,不容马虎!
  • 事务管理是企业级应用程序开发中必备技术,用来确保数据的完整性和一致性

事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用

事务四个属性 ACID

  • 原子性(atomicity)

事务是原子性操作,由一系列动作组成,事务的原子性确保动作要么全部完成,要么完全不起作用

  • 一致性(consistency)

一旦所有事务动作完成,事务就要被提交。数据和资源处于一种满足业务规则的一致性状态中

  • 隔离性(isolation)

可能多个事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏

  • 持久性(durability)

事务一旦完成,无论系统发生什么错误,结果都不会受到影响。通常情况下,事务的结果被写到持久化存储器中

测试

在之前的案例中,我们给 userMapper 接口新增三个方法,删除和增加用户;

1
2
3
4
5
6
7
//添加一个用户
int addUser(User user);

//根据id删除用户
int deleteUser(int id);

public List<User> test();

编写接口的实现类,在实现类中,我们去操作一波

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
public class UserMapperImpl extends SqlSessionDaoSupport implements UserMapper {

//增加一些操作
public List<>User test() {
UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
User user = new User(5,"小明","123456");
mapper.addUser(user);
mapper.deleteUser(5);
return mapper.selectUser();
}

public List<User> selectUser() {
UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
return mapper.selectUser();
}

//新增
public int addUser(User user) {
UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
return mapper.addUser(user);
}
//删除
public int deleteUser(int id) {
UserMapper mapper = getSqlSession().getMapper(UserMapper.class);
return mapper.deleteUser(id);
}

}

UserMapper.xml 配置文件中添加映射,并且故意写错 deletes

1
2
3
4
5
6
7
8
9
<insert id="addUser" resultType="User">
insert into mybatis.user (id, name, pwd) values (#{id},#{name},#{pwd})
</insert>

<delete id="deleteUser" resultType="User">
deletes from mybatis.user where id=#{id}
</delete>

<select id="test" resultType="User"/>

测试

1
2
3
4
5
6
7
8
9
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserMapper mapper = (UserMapper) context.getBean("userMapper");
List<User> userlist = mapper.test();
for (User user : userlist) {
System.out.println(user);
}
}

报错为 sql 异常,delete 错写

结果 :插入成功

没有进行事务的管理;我们想让他们都成功才成功,有一个失败,就都失败,我们就应该需要事务

以前我们都需要自己手动管理事务,十分麻烦

但是 Spring 给我们提供了事务管理,我们只需要配置即可;

Spring 中的事务管理

Spring 在不同的事务管理 API 之上定义了一个抽象层,使得开发人员不必了解底层的事务管理 API 就可以使用 Spring 的事务管理机制。Spring 支持编程式事务管理和声明式的事务管理

编程式事务管理

  • 将事务管理代码嵌到业务方法中来控制事务的提交和回滚
  • 缺点:必须在每个事务操作业务逻辑中包含额外的事务管理代码

声明式事务管理

  • 一般情况下比编程式事务好用
  • 将事务管理代码从业务方法中分离出来,以声明的方式来实现事务管理
  • 将事务管理作为横切关注点,通过 aop 方法模块化。Spring 中通过 Spring AOP 框架支持声明式事务管理

使用 Spring 管理事务,注意头文件的约束导入 : tx

1
2
3
4
xmlns:tx="http://www.springframework.org/schema/tx"

http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

事务管理器

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

JDBC 事务

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

配置好事务管理器后我们需要去配置事务的通知

1
2
3
4
5
6
7
8
9
10
11
12
<!--配置事务通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--配置哪些方法使用什么样的事务,配置事务的传播特性-->
<tx:method name="add" propagation="REQUIRED"/>
<tx:method name="delete" propagation="REQUIRED"/>
<tx:method name="update" propagation="REQUIRED"/>
<tx:method name="search*" propagation="REQUIRED"/>
<tx:method name="get" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>

配置 AOP

导入 aop 的头文件

1
2
3
4
5
<!--配置aop织入事务-->
<aop:config>
<aop:pointcut id="txPointCut" expression="execution(* com.example.mapper.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>

最终我们的 spring-dao.xml 配置文件完整如下:

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
<?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:tx="http://www.springframework.org/schema/tx"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis?useSSL=true&amp;useUnicode=true&amp;characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value=""/>
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" />
<!--绑定MyBatis配置文件-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:com/example/mapper/UserMapper.xml"/>
</bean>

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource" />
</bean>

<!--结合AOP实现事务的织入-->
<!--配置事务的类:-->
<!--配置事务通知:-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!--给哪些方法配置事务-->
<!--配置事务的传播特性-->
<tx:attributes>
<tx:method name="add" propagation="REQUIRED"/>
<tx:method name="delete" propagation="REQUIRED"/>
<tx:method name="query" read-only="true"/>
<tx:method name="*" propagation="REQUIRED"/>
</tx:attributes>
</tx:advice>

<!--配置事务切入-->
<aop:config>
<aop:pointcut id="txPointCut" expression="execution(* com.example.mapper.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
</aop:config>
</beans>

进行测试

删掉刚才插入的数据,再次测试:

1
2
3
4
5
6
7
8
9
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserMapper mapper = (UserMapper) context.getBean("userMapper");
List<User> userlist = mapper.test();
for (User user : userlist) {
System.out.println(user);
}
}

这时候仍然报错但是插入没成功

小结

为什么需要配置事务

  • 如果不配置事务,可能存在数据提交不一致的情况下;
  • 如果不在 spring 中去配置声明式事务,我们就需要在代码中手动配置事务
  • 事务在项目的开发中非常重要,涉及到数据的一致性和完整性问题!

https://drun1baby.top/2022/08/18/Spring%E5%BC%80%E5%8F%91%E5%AD%A6%E4%B9%A0/

https://juejin.cn/post/7546054937026330665

https://blog.csdn.net/not_give_up_/article/details/77200671