Spring官网:https://spring.io/

参考视频:

源码仓库:muyoukule/accidence-spring (github.com)

0. Spring简介

Spring是一个开源的Java EE应用程序框架,由Rod Johnson在2002年创建,旨在解决企业级编程开发中的复杂性,实现敏捷开发。Spring框架是一个轻量级的容器,用于管理业务相关的对象。它通过控制反转(IoC)和面向切面编程(AOP)等特性,降低了组件之间的耦合度,提高了代码的可重用性和可维护性。

核心特性

  • 依赖注入(Dependency Injection):这是Spring框架管理对象间依赖关系的主要手段。通过依赖注入,我们可以将对象的依赖关系从代码中解耦出来,交由Spring容器来管理。这样,当依赖关系发生变化时,我们只需要修改配置文件,而无需修改代码。
  • 面向切面编程(Aspect-Oriented Programming,AOP):Spring AOP通过定义横切关注点(如日志、安全、事务管理等),将这些关注点从业务逻辑中分离出来,以模块化的方式进行处理。这有助于减少代码的重复,提高代码的可读性和可维护性。
  • 事务管理:Spring框架提供了强大的事务管理功能,可以轻松地处理事务操作,确保数据的完整性和一致性。
  • 数据访问:Spring框架对多种数据访问技术提供了良好的支持,如JDBC、ORM框架(如Hibernate、MyBatis)等,简化了数据访问层的开发。

应用场景

Spring框架广泛应用于各种Java企业级应用程序开发中,包括Web应用、RESTful服务、批处理应用等。无论是构建大型的分布式系统,还是开发小型的应用程序,Spring都能提供强大的支持和灵活的解决方案。

模块组成

Spring框架由多个模块组成,每个模块都有其特定的功能和用途。核心容器(Core Container)是其他模块建立的基础,主要由Beans模块、Core模块、Context模块、Context-support模块和SpEL(Spring Expression Language)模块组成。此外,还有数据访问/集成(Data Access/Integration)层,包括JDBC、ORM、OXM、JMS和Transactions模块等。

使用方式

在使用 Spring 框架时,一般通过 Maven 或 Gradle 等构建工具导入所需的依赖。然后,我们可以开始编写 Spring 应用程序,利用Spring 的 IoC 和 AOP 等特性来管理对象间的依赖关系和横切关注点。同时,我们还可以利用 Spring 提供的数据访问和事务管理等功能来简化应用程序的开发。

1. 入门程序

1.1 IOC入门案例

1.1.1 准备工作

a. 打开IDEA创建Empty Project:spring

b. 设置JDK版本17,编译器版本17

c. 设置IDEA的Maven:关联自己的Maven

d. 在空的工程中创建第一个模块:spring-001

e. 创建好的项目结构如下:

1.1.2 创建项目

a. 在 pom.xml 添加 Spring 的依赖jar包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<!--spring context依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.27</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>

当加入 spring context 的依赖之后,会关联引入其他依赖:

  • spring aop :面向切面编程

  • spring beans :IoC核心

  • spring core :spring的核心工具包

  • spring jcl :spring的日志包

  • spring expression :spring表达式

b. 添加案例中需要的类,创建 User

1
2
3
4
5
public class User {
public void hello() {
System.out.println("hello userBean....");
}
}

c. 添加spring配置文件 applicationContext.xml

d. 在 applicationContext.xml 配置文件中完成 bean 的配置

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标签标示配置bean,id属性标示给bean起名字,class属性表示给bean定义类型-->
<bean id="userBean" class="com.muyoukule.Bean.User"/>

</beans>

e. 获取IOC容器

使用Spring提供的接口完成IOC容器的创建,创建App类,编写main方法

1
2
3
4
5
6
public class App {
public static void main(String[] args) {
//获取IOC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
}
}

f. 从容器中获取对象进行方法调用

1
2
3
4
5
6
7
8
9
10
11
12
public class App {
public static void main(String[] args) {
//获取IOC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//获取出的对象为Object类型,需要进行强转
//User userBean = (User) ctx.getBean("userBean");

//或者在参数后指定返回值的类型
User userBean = ctx.getBean("userBean", User.class);
userBean.hello();
}
}

g. 运行 main() 方法

1
hello userBean....

至此,Spring的IOC入门案例已经完成。

1.2 IOC入门案例详解

Bean 标签的 id 属性可以重复吗?

新建一个 Vip

1
2
public class Vip {
}

applicationContext.xml 配置文件中配置为 bean

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标签标示配置bean,id属性标示给bean起名字,class属性表示给bean定义类型-->
<bean id="userBean" class="com.muyoukule.Bean.User"/>
<bean id="userBean" class="com.muyoukule.Bean.Vip"/>

</beans>

运行 main() 方法测试,程序报错提示:

通过测试得出:在 spring 的配置文件中 id 是不能重名。

底层是怎么创建对象的,是通过反射机制调用无参数构造方法吗?

User 类中添加无参构造方法

1
2
3
4
5
6
7
8
9
public class User {
public User() {
System.out.println("User的无参数构造方法执行");
}

public void hello() {
System.out.println("hello userBean....");
}
}

运行测试程序

1
2
User的无参数构造方法执行
hello userBean....

通过测试得知:创建对象时确实调用了无参数构造方法。

如果提供一个有参数构造方法,不提供无参数构造方法会怎样呢?

注释掉 User 类中的无参构造方法,在 User 类中添加有参构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class User {
/* public User() {
System.out.println("User的无参数构造方法执行");
}*/

public User(String name) {
System.out.println("User的有参数构造方法执行");
}

public void hello() {
System.out.println("hello userBean....");
}
}

运行测试程序

通过测试得知:spring 是通过调用类的无参数构造方法来创建对象的,所以要想让 spring 给你创建对象,必须保证无参数构造方法是存在的。

Spring是如何创建对象的呢?原理是什么?

1
2
3
4
// dom4j解析beans.xml文件,从中获取class的全限定类名
// 通过反射机制调用无参数构造方法创建对象
Class clazz = Class.forName("com.muyoukule.Bean.User");
Object obj = clazz.newInstance();

把创建好的对象存储到一个什么样的数据结构当中了呢?

spring配置文件的名字可以随意命名,且可以创建多个,并且在读取的时候也可以一并读取

applicationContext2.xml 配置文件中配置 vipBean

1
<bean id="vipBean" class="com.muyoukule.Bean.Vip"/>
1
2
3
4
5
6
7
8
9
10
11
public class App {
public static void main(String[] args) {
//获取IOC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml", "applicationContext2.xml");
//或者在参数后指定返回值的类型
User userBean = ctx.getBean("userBean", User.class);
System.out.println(userBean);
Vip vipBean = ctx.getBean("vipBean", Vip.class);
System.out.println(vipBean);
}
}

运行测试程序

1
2
3
User的无参数构造方法执行
com.muyoukule.Bean.User@345965f2
com.muyoukule.Bean.Vip@429bd883

在配置文件中配置的类必须是自定义的吗,可以使用JDK中的类吗,例如:java.util.Date?

applicationContext.xml 配置文件中添加如下 bean

1
<bean id="dateBean" class="java.util.Date"/>
1
2
3
4
5
6
7
8
9
10
11
public class App {
public static void main(String[] args) {
//获取IOC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//或者在参数后指定返回值的类型
User userBean = ctx.getBean("userBean", User.class);
System.out.println(userBean);
Object dateBean = ctx.getBean("dateBean");
System.out.println(dateBean);
}
}
1
2
3
User的无参数构造方法执行
com.muyoukule.Bean.User@4d339552
Mon May 08 23:36:09 CST 2023

通过测试得知,在 spring 配置文件中配置的bean可以任意类,只要这个类不是抽象的,并且提供了无参数构造方法。

getBean() 方法返回的类型是 Object ,如果访问子类的特有属性和方法时,还需要向下转型,有其它办法可以解决这个问题吗?

1
User user = applicationContext.getBean("userBean", User.class);

ClassPathXmlApplicationContext 是从类路径中加载配置文件,如果没有在类路径当中,又应该如何加载配置文件呢?

1
ApplicationContext applicationContext = new FileSystemXmlApplicationContext("d:/applicationContext.xml");

没有在类路径中的话,需要使用 FileSystemXmlApplicationContext 类进行加载配置文件。这种方式较少用。一般都是将配置文件放到类路径当中,这样可移植性更强。

ApplicationContext 的超级父接口 BeanFactory

1
2
3
BeanFactory beanFactory = new ClassPathXmlApplicationContext("applicationContext.xml");
Object vipBean = beanFactory.getBean("vipBean");
System.out.println(vipBean);

BeanFactory 是 Spring 容器的超级接口。ApplicationContextBeanFactory 的子接口。

1.3 Spring启用Log4j2日志框架

从Spring5之后,Spring框架支持集成的日志框架是 Log4j2 。如何启用日志框架:

a. 引入 Log4j2 的依赖

1
2
3
4
5
6
7
8
9
10
11
<!--log4j2的依赖-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.19.0</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.19.0</version>
</dependency>

b. 在类的根路径下提供 log4j2.xml 配置文件(文件名固定为:log4j2.xml ,文件必须放到类根路径下。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<loggers>
<!--
level指定日志级别,从低到高的优先级:
ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF
-->
<root level="DEBUG">
<appender-ref ref="springlog"/>
</root>
</loggers>

<appenders>
<!--输出日志信息到控制台-->
<console name="springlog" target="SYSTEM_OUT">
<!--控制日志输出的格式-->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/>
</console>
</appenders>

</configuration>

2. Spring对IoC的实现

2.1 IoC 控制反转

  • 控制反转是一种思想。

  • 控制反转是为了降低程序耦合度,提高程序扩展力,达到OCP原则,达到DIP原则。

  • 控制反转,反转的是什么?

    • 将对象的创建权利交出去,交给第三方容器负责。
    • 将对象和对象之间关系的维护权交出去,交给第三方容器负责。
  • 控制反转这种思想如何实现呢?

    • DI(Dependency Injection):依赖注入

2.2 依赖注入

依赖注入实现了控制反转的思想。Spring通过依赖注入的方式来完成Bean管理的。

Bean管理说的是:Bean 对象的创建,以及Bean对象中属性的赋值(或者叫做 Bean 对象之间关系的维护)。

依赖注入:

  • 依赖指的是对象和对象之间的关联关系。
  • 注入指的是一种数据传递行为,通过注入行为来让对象和对象产生关系。

依赖注入常见的实现方式包括两种:

  • 第一种:set注入
  • 第二种:构造注入

新建模块:spring-002

2.2.1 set注入

set 注入,基于 set 方法实现的,底层会通过反射机制调用属性对应的 set 方法然后给属性赋值。这种方式要求属性必须对外提供 set 方法。

对外提供 set 方法

a. 创建 UserDao

1
2
3
4
5
public class UserDao {
public void insert() {
System.out.println("正在保存用户数据...");
}
}

b. 创建 UserService 类,并为 UserDao 提供 setter 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserService {
private UserDao userDao;

// 使用set方式注入,必须提供set方法。
// 反射机制要调用这个方法给属性赋值的。
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

public void save() {
System.out.println("UserService...");
userDao.insert();
}
}

c. 在配置文件中完成 bean 的配置

1
2
3
4
5
6
<bean id="userDaoBean" class="com.muyoukule.Dao.UserDao"/>

<!--property标签表示配置当前bean的属性,name属性表示配置哪一个具体的属性,ref属性表示参照哪一个bean-->
<bean id="userServiceBean" class="com.muyoukule.Service.UserService">
<property name="userDao" ref="userDaoBean"/>
</bean>

d. 编写测试程序

1
2
3
4
5
6
@Test
public void testSetDI() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
UserService userService = applicationContext.getBean("userServiceBean", UserService.class);
userService.save();
}

e. 运行结果

1
2
UserService...
正在保存用户数据...

实现原理:

  • 通过 property 标签获取到属性名:userDao

  • 通过属性名推断出set方法名:setUserDao

  • 通过反射机制调用 setUserDao() 方法给属性赋值。

  • property 标签的 name 是属性名。

  • property 标签的 ref 是要注入的 bean 对象的 id。(通过 ref 属性来完成 bean 的装配,这是 bean最简单的一种装配方式。装配指的是:创建系统组件之间关联的动作)

把set方法注释掉

通过测试得知,底层实际上调用了 setUserDao() 方法。所以需要确保这个方法的存在。

把属性名修改一下,但方法名还是 setUserDao()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserService {
private UserDao aaa;

// 使用set方式注入,必须提供set方法。
// 反射机制要调用这个方法给属性赋值的。
public void setUserDao(UserDao userDao) {
this.aaa = userDao;
}

public void save() {
System.out.println("UserService...");
aaa.insert();
}
}
1
2
UserService...
正在保存用户数据...

通过测试看到程序仍然可以正常执行,说明 property 标签的 name 是:setUserDao() 方法名演变得到的。演变的规律是:

  • setUsername() 演变为 username
  • setUserDao() 演变为 userDao

另外,对于 property 标签来说,ref 属性也可以采用标签的方式,但使用 ref 属性是多数的。

1
2
3
4
5
<bean id="userServiceBean" class="com.muyoukule.Service.UserService">
<property name="userDao">
<ref bean="userDaoBean"/>
</property>
</bean>

总结:set 注入的核心实现原理:通过反射机制调用 set 方法来给属性赋值,让两个对象之间产生关系。

2.2.2 构造注入

核心原理:通过调用构造方法来给属性赋值。

提供构造方法

a. 创建如下两个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class OrderDao {
public void deleteById() {
System.out.println("正在删除订单...");
}
}

public class OrderService {
private OrderDao orderDao;

// 通过反射机制调用构造方法给属性赋值
public OrderService(OrderDao orderDao) {
this.orderDao = orderDao;
}

public void delete() {
System.out.println("OrderService...");
orderDao.deleteById();
}
}

b. 在 spring.xml 下配置如下 bean

1
2
3
4
5
<bean id="orderDaoBean" class="com.muyoukule.Dao.OrderDao"/>
<bean id="orderServiceBean" class="com.muyoukule.Service.OrderService">
<!--index="0"表示构造方法的第一个参数,将orderDaoBean对象传递给构造方法的第一个参数。-->
<constructor-arg index="0" ref="orderDaoBean"/>
</bean>

c. 编写测试程序

1
2
3
4
5
6
@Test
public void testConstructorDI() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
OrderService orderServiceBean = applicationContext.getBean("orderServiceBean", OrderService.class);
orderServiceBean.delete();
}

d. 运行结果

1
2
OrderService...
正在删除订单...

如果构造方法有两个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class OrderService {
private OrderDao orderDao;
private UserDao userDao;

// 通过反射机制调用构造方法给属性赋值
public OrderService(OrderDao orderDao, UserDao userDao) {
this.orderDao = orderDao;
this.userDao = userDao;
}

public void delete() {
System.out.println("OrderService...");
orderDao.deleteById();
userDao.insert();
}
}

配置文件

1
2
3
4
5
6
7
8
<bean id="orderDaoBean" class="com.muyoukule.Dao.OrderDao"/>
<bean id="userDaoBean" class="com.muyoukule.Dao.UserDao"/>
<bean id="orderServiceBean" class="com.muyoukule.Service.OrderService">
<!--第一个参数下标是0-->
<constructor-arg index="0" ref="orderDaoBean"/>
<!--第二个参数下标是1-->
<constructor-arg index="1" ref="userDaoBean"/>
</bean>

执行测试程序

1
2
3
OrderService...
正在删除订单...
正在保存用户数据...

不使用参数下标,使用参数的名字

1
2
3
4
5
6
7
<bean id="orderDaoBean" class="com.muyoukule.Dao.OrderDao"/>
<bean id="userDaoBean" class="com.muyoukule.Dao.UserDao"/>
<bean id="orderServiceBean" class="com.muyoukule.Service.OrderService">
<!--这里使用了构造方法上参数的名字-->
<constructor-arg name="orderDao" ref="orderDaoBean"/>
<constructor-arg name="userDao" ref="userDaoBean"/>
</bean>

不指定参数下标,不指定参数名字

1
2
3
4
5
6
7
<bean id="orderDaoBean" class="com.muyoukule.Dao.OrderDao"/>
<bean id="userDaoBean" class="com.muyoukule.Dao.UserDao"/>
<bean id="orderServiceBean" class="com.muyoukule.Service.OrderService">
<!--没有指定下标,也没有指定参数名字-->
<constructor-arg ref="orderDaoBean"/>
<constructor-arg ref="userDaoBean"/>
</bean>

配置文件中构造方法参数的类型顺序和构造方法参数的类型顺序不一致

1
2
3
4
5
6
7
<bean id="orderDaoBean" class="com.muyoukule.Dao.OrderDao"/>
<bean id="userDaoBean" class="com.muyoukule.Dao.UserDao"/>
<bean id="orderServiceBean" class="com.muyoukule.Service.OrderService">
<!--顺序已经和构造方法的参数顺序不同了-->
<constructor-arg ref="userDaoBean"/>
<constructor-arg ref="orderDaoBean"/>
</bean>

以上几种情况测试结果均为:

1
2
3
OrderService...
正在删除订单...
正在保存用户数据...

通过测试得知,通过构造方法注入的时候:

  • 可以通过下标
  • 可以通过参数名
  • 也可以不指定下标和参数名,可以类型自动推断。

2.3 set注入专题

2.3.1 注入外部 Bean

之前的一个案例中使用的案例就是注入外部 Bean 的方式。

1
2
3
4
5
6
<bean id="userDaoBean" class="com.muyoukule.Dao.UserDao"/>

<!--property标签表示配置当前bean的属性,name属性表示配置哪一个具体的属性,ref属性表示参照哪一个bean-->
<bean id="userServiceBean" class="com.muyoukule.Service.UserService">
<property name="userDao" ref="userDaoBean"/>
</bean>

外部Bean的特点:bean 定义到外面,在 property 标签中使用 ref 属性进行注入。通常这种方式是常用。

2.3.2 注入内部 Bean

内部 Bean 的方式:在 bean 标签中嵌套 bean 标签。

a. 新建 spring-inner-bean.xml 文件,配置以下 bean

1
2
3
4
5
<bean id="userServiceBean" class="com.muyoukule.Service.UserService">
<property name="userDao">
<bean class="com.muyoukule.Dao.UserDao"/>
</property>
</bean>

b. 编写测试代码

1
2
3
4
5
6
@Test
public void testInnerBean() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-inner-bean.xml");
UserService userService = applicationContext.getBean("userServiceBean", UserService.class);
userService.save();
}

c. 执行测试程序

1
2
UserService...
正在保存用户数据...

2.3.3 注入简单类型

我们之前在进行注入的时候,对象的属性是另一个对象。

1
2
3
4
5
6
7
8
9
10
//对象的属性是另一个对象
public class UserService{

private UserDao userDao;

public void setUserDao(UserDao userDao){
this.userDao = userDao;
}

}

对象的属性是 int 类型

1
2
3
4
5
6
7
8
9
10
//对象的属性是int类型
public class User{

private int age;

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

}

也是通过 set 注入的方式给该属性赋值,因为只要能够调用 set 方法就可以给属性赋值。

编写程序给一个 User 对象的 age 属性赋值20

a. 定义User类,提供 age 属性,提供 age 属性的 setter 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class User {
private int age;

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

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

b. 编写 spring 配置文件:spring-simple-type.xml

1
2
3
4
5
6
7
<bean id="userBean" class="com.muyoukule.Bean.User">
<!--如果像这种int类型的属性,我们称为简单类型,这种简单类型在注入的时候要使用value属性,不能使用ref-->
<!--<property name="age" value="20"/>-->
<property name="age">
<value>20</value>
</property>
</bean>

c. 编写测试程序

1
2
3
4
5
6
@Test
public void testSimpleType(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-simple-type.xml");
User user = applicationContext.getBean("userBean", User.class);
System.out.println(user);
}

d. 运行测试程序

1
User{age=20}

需要特别注意:如果给简单类型赋值,使用 value 属性或 value 标签。而不是 ref

简单类型包括哪些呢?

可以通过 Spring 的源码来分析一下:BeanUtils

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
public class BeanUtils{

// --snip--

/**
* Check if the given type represents a "simple" property: a simple value
* type or an array of simple value types.
* <p>See {@link #isSimpleValueType(Class)} for the definition of <em>simple
* value type</em>.
* <p>Used to determine properties to check for a "simple" dependency-check.
* @param type the type to check
* @return whether the given type represents a "simple" property
* @see org.springframework.beans.factory.support.RootBeanDefinition#DEPENDENCY_CHECK_SIMPLE
* @see org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#checkDependencies
* @see #isSimpleValueType(Class)
*/
public static boolean isSimpleProperty(Class<?> type) {
Assert.notNull(type, "'type' must not be null");
return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.getComponentType()));
}

/**
* Check if the given type represents a "simple" value type: a primitive or
* primitive wrapper, an enum, a String or other CharSequence, a Number, a
* Date, a Temporal, a URI, a URL, a Locale, or a Class.
* <p>{@code Void} and {@code void} are not considered simple value types.
* @param type the type to check
* @return whether the given type represents a "simple" value type
* @see #isSimpleProperty(Class)
*/
public static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}

// --snip--
}

通过源码分析得知,简单类型包括:

基本数据类型、基本数据类型对应的包装类、String 或其他的 CharSequence 子类、Number 子类、Date 子类、Enum 子类、URIURLTemporal 子类、LocaleClass 、另外还包括以上简单值类型对应的数组类型。

测试Data类型

a. 编写类:

1
2
3
4
5
6
@Setter
@ToString
public class SimpleValueType {
private Date birth;

}

b. 编写配置文件:

1
2
3
4
5
6
7
<bean id="svt" class="com.muyoukule.Bean.SimpleValueType">
<!--报错了,说1970-10-11这个字符串无法转换成java.util.Date类型。-->
<!--<property name="birth" value="1970-10-11"/>-->
<!--如果你硬要把Date当做简单类型的话,使用value赋值的话,这个日期字符串格式有要求-->
<!--在实际开发中,我们一般不会把Date当做简单类型,虽然它是简单类型。一般会采用ref给Date类型的属性赋值。-->
<property name="birth" value="Wed Oct 19 16:28:13 CST 2022"/>
</bean>

c. 编写测试类:

1
2
3
4
5
6
@Test
public void testSimpleTypeSet() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("date.xml");
SimpleValueType svt = applicationContext.getBean("svt", SimpleValueType.class);
System.out.println(svt);
}

d. 运行测试程序:

1
SimpleValueType(birth=Thu Oct 20 06:28:13 CST 2022)

需要注意的是:

  • 如果把 Date 当做简单类型的话,日期字符串格式不能随便写。格式必须符合 DatetoString() 方法格式。显然这就比较鸡肋了。如果我们提供一个这样的日期字符串:2010-10-11,在这里是无法赋值给 Date 类型的属性的。
  • spring6之后,当注入的是 URL,那么这个 url 字符串是会进行有效性检测的。如果是一个存在的 url ,那就没问题。如果不存在则报错。
  • 对于引用数据类型使用的是 <property name="" ref=""/>
  • 对于简单数据类型使用的是 <property name="" value=""/>

2.3.4 级联属性赋值(了解)

a. 创建如下类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Clazz {
private String name;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student {
private String name;
private Clazz clazz;
}

b. 编写配置文件

1
2
3
4
5
6
7
8
<bean id="clazzBean" class="com.muyoukule.Bean.Clazz"/>
<bean id="student" class="com.muyoukule.Bean.Student">
<property name="name" value="张三"/>
<!--要点1:以下两行配置的顺序不能颠倒-->
<property name="clazz" ref="clazzBean"/>
<!--要点2:clazz属性必须有getter方法-->
<property name="clazz.name" value="高三一班"/>
</bean>

c. 编写测试类

1
2
3
4
5
6
@Test
public void testCascade() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-cascade.xml");
Student student = applicationContext.getBean("student", Student.class);
System.out.println(student);
}

d. 运行测试程序

1
Student(name=张三, clazz=Clazz(name=高三一班))

要点:

  • 在 spring 配置文件中,注意顺序,配置的顺序不能颠倒。
  • 在 spring 配置文件中,clazz 属性必须提供 getter 方法。

2.3.5 注入数组

数组中的元素是简单类型

a. 编写Person类

1
2
3
4
5
@Setter
@ToString
public class Person {
private String[] favoriteFoods;
}

b. spring-array-simple.xml

1
2
3
4
5
6
7
8
9
<bean id="person" class="com.zxq.bean.Person">
<property name="favoriteFoods">
<array>
<value>鸡排</value>
<value>汉堡</value>
<value>鹅肝</value>
</array>
</property>
</bean>

c. 编写测试类

1
2
3
4
5
6
@Test
public void testArraySimple() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-array-simple.xml");
Person person = applicationContext.getBean("person", Person.class);
System.out.println(person);
}

d. 运行测试程序

1
Person(favoriteFoods=[鸡排, 汉堡, 鹅肝])

数组中的元素是非简单类型

a. 创建如下类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Goods {
private String name;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
// 一个订单中有多个商品
private Goods[] goods;
}

b. spring-array.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<bean id="goods1" class="com.muyoukule.Bean.Goods">
<property name="name" value="西瓜"/>
</bean>

<bean id="goods2" class="com.muyoukule.Bean.Goods">
<property name="name" value="苹果"/>
</bean>

<bean id="order" class="com.muyoukule.Bean.Order">
<property name="goods">
<array>
<!--这里使用ref标签即可-->
<ref bean="goods1"/>
<ref bean="goods2"/>
</array>
</property>
</bean>

c. 编写测试类

1
2
3
4
5
6
@Test
public void testArray() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-array.xml");
Order order = applicationContext.getBean("order", Order.class);
System.out.println(order);
}

d. 运行测试程序

1
Order(goods=[Goods(name=西瓜), Goods(name=苹果)])

要点:

  • 如果数组中是简单类型,使用 value 标签。
  • 如果数组中是非简单类型,使用 ref 标签。

2.3.6 注入 List 集合、Set 集合、Map 集合、Properties

List 集合:有序可重复; Set 集合:无序不可重复。

java.util.Properties 继承 java.util.Hashtable ,所以 Properties 也是一个 Map 集合。

a. 创建如下类

1
2
3
4
5
6
7
8
@Setter
@ToString
public class People {
private List<String> names;
private Set<String> phones;
private Map<Integer, String> addrs;
private Properties properties;
}

b. spring-collection.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
<bean id="peopleBean" class="com.muyoukule.Bean.People">
<property name="names">
<list>
<value>铁锤</value>
<value>张三</value>
<value>张三</value>
<value>张三</value>
<value></value>
</list>
</property>
<property name="phones">
<set>
<!--非简单类型可以使用ref,简单类型使用value-->
<value>110</value>
<value>110</value>
<value>120</value>
<value>120</value>
<value>119</value>
<value>119</value>
</set>
</property>
<property name="addrs">
<map>
<!--如果key不是简单类型,使用 key-ref 属性-->
<!--如果value不是简单类型,使用 value-ref 属性-->
<entry key="1" value="北京大兴区"/>
<entry key="2" value="上海浦东区"/>
<entry key="3" value="深圳宝安区"/>
</map>
</property>
<property name="properties">
<props>
<prop key="driver">com.mysql.cj.jdbc.Driver</prop>
<prop key="url">jdbc:mysql://localhost:3306/spring</prop>
<prop key="username">root</prop>
<prop key="password">123456</prop>
</props>
</property>
</bean>

c. 编写测试类

1
2
3
4
5
6
@Test
public void testCollection() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-collection.xml");
People peopleBean = applicationContext.getBean("peopleBean", People.class);
System.out.println(peopleBean);
}

d. 运行测试程序

1
People(names=[铁锤, 张三, 张三, 张三, 狼], phones=[110, 120, 119], addrs={1=北京大兴区, 2=上海浦东区, 3=深圳宝安区}, properties={password=123456, driver=com.mysql.cj.jdbc.Driver, url=jdbc:mysql://localhost:3306/spring, username=root})

注入 List 、Set 集合:

  • 使用 标签
  • 集合中元素是简单类型的使用 value 标签,反之使用 ref 标签。

注入 Map 集合:

  • 使用 标签
  • 如果 key 是简单类型,使用 key 属性,反之使用 key-ref 属性。
  • 如果 value 是简单类型,使用 value 属性,反之使用 value-ref 属性。

注入 Properties:

  • 使用 props 标签嵌套 prop 标签完成。

2.3.7 注入null和空字符串

注入空字符串使用: 或者 value=””

a. 创建如下类

1
2
3
4
5
@Setter
@ToString
public class Vip {
private String email;
}

b. spring-null.xml

1
2
3
4
5
6
7
8
<bean id="vipBean" class="com.muyoukule.Bean.Vip">
<!--空串的第一种方式-->
<!-- <property name="email" value=""/>-->
<!--空串的第二种方式-->
<property name="email">
<value/>
</property>
</bean>

c. 编写测试类

1
2
3
4
5
6
@Test
public void testNull() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-null.xml");
Vip vipBean = applicationContext.getBean("vipBean", Vip.class);
System.out.println(vipBean);
}

d. 运行测试程序

1
Vip(email=)

注入null使用: 或者 不为该属性赋值

a. 修改 spring-null.xml

1
2
3
4
5
6
7
8
<!--第一种方式:不给属性赋值-->
<!--<bean id="vipBean" class="com.zxq.Bean.Vip"/>-->
<!--第二种方式:使用<null/>-->
<bean id="vipBean" class="com.muyoukule.Bean.Vip">
<property name="email">
<null/>
</property>
</bean>

b. 运行测试类

1
Vip(email=null)

2.3.8 注入的值中含有特殊符号

XML中有5个特殊字符,分别是:< 、> 、’ 、” 、&

以上5个特殊符号在 XML 中会被特殊对待,会被当做 XML 语法的一部分进行解析,如果这些特殊符号直接出现在注入的字符串当中,会报错。

解决方案包括两种:

  • 第一种:特殊符号使用转义字符代替。
  • 第二种:将含有特殊符号的字符串放到:<![CDATA[]]> 当中。因为放在CDATA区中的数据不会被XML文件解析器解析。

5个特殊字符对应的转义字符分别是:

特殊字符 转义字符
> &gt;
< &lt;
&apos;
&quot;
& &amp;

a. 创建如下类

1
2
3
4
5
@Setter
@ToString
public class Math {
private String result;
}

b. spring-special.xml

使用转义字符代替方式

1
2
3
<bean id="mathBean" class="com.muyoukule.Bean.Math">
<property name="result" value="2 &lt; 3"/>
</bean>

使用CDATA方式

1
2
3
4
5
6
<bean id="mathBean" class="com.muyoukule.Bean.Math">
<property name="result">
<!--只能使用value标签-->
<value><![CDATA[2 < 3]]></value>
</property>
</bean>

c. 编写测试类

1
2
3
4
5
6
7
@Test
public void testSpecial() {
//这里注意导入 Math 包的时候不要导成 java.lang.Math 了
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-special.xml");
Math mathBean = applicationContext.getBean("mathBean", Math.class);
System.out.println(mathBean);
}

d. 运行测试类

1
Math(result=2 < 3)

使用 CDATA 时,不能使用 value 属性,只能使用 value 标签。

2.4 p命名空间注入

目的:简化配置。

p命名空间实际上是对 set 注入的简化。

使用p命名空间注入的前提条件包括两个:

  • 在XML头部信息中添加p命名空间的配置信息:xmlns:p=”http://www.springframework.org/schema/p
  • p命名空间注入是基于 setter 方法的,所以需要对应的属性提供setter方法。

新建spting_003模块

a. 创建如下类

1
2
3
4
5
6
7
@Setter
@ToString
public class Customer {
//p命名空间注入是基于setter方法的,所以需要对应的属性提供setter方法
private String name;
private int age;
}

b. spring-p.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:p="http://www.springframework.org/schema/p"
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="customerBean" class="com.muyoukule.Bean.Customer" p:name="zhangsan" p:age="20"/>

</beans>

c. 编写测试类

1
2
3
4
5
6
@Test
public void testP() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-p.xml");
Customer customerBean = applicationContext.getBean("customerBean", Customer.class);
System.out.println(customerBean);
}

d. 运行测试类

1
Customer(name=zhangsan, age=20)

setter 方法去掉(注释掉 @Setter ):

2.5 c命名空间注入

目的:简化配置。

c命名空间是简化构造方法注入的。

使用c命名空间的两个前提条件:

a. 创建如下类

1
2
3
4
5
6
7
8
@AllArgsConstructor
@ToString
public class MyTime {
//需要提供构造方法
private int year;
private int month;
private int day;
}

b. spring-c.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:c="http://www.springframework.org/schema/c"
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="myTimeBean" class="com.muyoukule.Bean.MyTime" c:year="1970" c:month="1" c:day="5"/>-->
<bean id="myTimeBean" class="com.muyoukule.Bean.MyTime" c:_0="2008" c:_1="8" c:_2="8"/>

</beans>

c. 编写测试类

1
2
3
4
5
6
@Test
public void testC() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-c.xml");
MyTime myTimeBean = applicationContext.getBean("myTimeBean", MyTime.class);
System.out.println(myTimeBean);
}

d. 运行测试类

1
MyTime(year=2008, month=8, day=8)

把构造方法注释掉(注释掉 @AllArgsConstructor ):

注意:不管是p命名空间还是c命名空间,注入的时候都可以注入简单类型以及非简单类型。

2.6 util命名空间

使用 util 命名空间可以让配置复用。

使用 util 命名空间的前提是:在spring配置文件头部添加配置信息。如下:

a. 创建如下类

1
2
3
4
5
6
7
8
9
10
11
@Setter
@ToString
public class MyDataSource1 {
private Properties properties;
}

@Setter
@ToString
public class MyDataSource2 {
private Properties properties;
}

b. spring-util.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

<util:properties id="prop">
<prop key="driver">com.mysql.cj.jdbc.Driver</prop>
<prop key="url">jdbc:mysql://localhost:3306/spring</prop>
<prop key="username">root</prop>
<prop key="password">root</prop>
</util:properties>

<bean id="dataSource1" class="com.muyoukule.Bean.MyDataSource1">
<property name="properties" ref="prop"/>
</bean>
<bean id="dataSource2" class="com.muyoukule.Bean.MyDataSource2">
<property name="properties" ref="prop"/>
</bean>

</beans>

c. 编写测试类

1
2
3
4
5
6
7
8
@Test
public void testUtil() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-util.xml");
MyDataSource1 dataSource1 = applicationContext.getBean("dataSource1", MyDataSource1.class);
System.out.println(dataSource1);
MyDataSource2 dataSource2 = applicationContext.getBean("dataSource2", MyDataSource2.class);
System.out.println(dataSource2);
}

d. 运行测试类

1
2
MyDataSource1(properties={password=root, driver=com.mysql.cj.jdbc.Driver, url=jdbc:mysql://localhost:3306/spring, username=root})
MyDataSource2(properties={password=root, driver=com.mysql.cj.jdbc.Driver, url=jdbc:mysql://localhost:3306/spring, username=root})

2.7 基于XML的自动装配

Spring还可以完成自动化的注入,自动化注入又被称为自动装配。它可以根据名字进行自动装配,也可以根据类型进行自动装配。

2.7.1 根据名称自动装配

a. 创建如下类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class UserDao {
public void insert() {
System.out.println("正在保存用户数据...");
}
}

public class UserService {
private UserDao aaa;
// 这个set方法非常关键
public void setAaa(UserDao aaa) {
this.aaa = aaa;
}
public void save() {
System.out.println("UserService...");
aaa.insert();
}
}

b. spring-autowire.xml

1
2
3
<!--byName表示根据名字自动装配-->
<bean id="userService" class="com.muyoukule.Service.UserService" autowire="byName"/>
<bean id="aaa" class="com.muyoukule.Dao.UserDao"/>

这个配置起到关键作用:

  • UserService Bean中需要添加 autowire="byName",表示通过名称进行装配。
  • UserService 类中有一个 UserDao 属性,而UserDao属性的名字是aaa,对应的 set 方法是 setAaa() ,正好和 UserDao Bean 的 id 是一样的。这就是根据名称自动装配。

c. 编写测试类

1
2
3
4
5
6
@Test
public void testAutowireByName() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowire.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.save();
}

d. 运行测试类

1
2
UserService...
正在保存用户数据...

测试一下,byName 装配是和属性名有关还是和set方法名有关系:

1
2
3
4
5
6
7
8
9
10
public class UserService {
private UserDao aaa;

// set方法名变化了
public void setDao(UserDao aaa) {
this.aaa = aaa;
}

// --snip--
}

再执行测试程序

通过测试得知,aaa 属性并没有赋值成功。也就是并没有装配成功。

将 spring 配置文件修改以下:

1
2
<!--这个id修改了-->
<bean id="dao" class="com.muyoukule.Dao.UserDao"/>

执行测试程序

1
2
UserService...
正在保存用户数据...

这说明,如果根据名称装配(byName),底层会调用 set 方法进行注入。

例如:setAge() 对应的名字是 age,setPassword() 对应的名字是 password,setEmail() 对应的名字是 email。

2.7.2 根据类型自动装配

a. 创建如下类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AccountDao {
public void insert() {
System.out.println("正在保存账户信息...");
}
}

public class AccountService {
private AccountDao accountDao;
public void setAccountDao(AccountDao accountDao) {
this.accountDao = accountDao;
}
public void save() {
System.out.println("AccountService...");
accountDao.insert();
}
}

b. spring-autowire.xml

1
2
3
<!--byType表示根据类型自动装配-->
<bean id="accountService" class="com.muyoukule.Service.AccountService" autowire="byType"/>
<bean class="com.muyoukule.Dao.AccountDao"/>

c. 编写测试类

1
2
3
4
5
6
@Test
public void testAutowireByType() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-autowire.xml");
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
accountService.save();
}

d. 运行测试类

1
2
AccountService...
正在保存账户信息...

AccountService 中的 set 方法注释掉,再执行:

可以看到无论是 byName 还是 byType ,在装配的时候都是基于 set 方法的。所以 set 方法是必须要提供的。提供构造方法是不行的。

如果 byType,根据类型装配时,如果配置文件中有两个类型一样的 bean 会出现什么问题呢?

修改spring-autowire.xml

1
2
3
4
<!--byType表示根据类型自动装配-->
<bean id="accountService" class="com.muyoukule.Service.AccountService" autowire="byType"/>
<bean id="x" class="com.muyoukule.Dao.AccountDao"/>
<bean id="y" class="com.muyoukule.Dao.AccountDao"/>

显然当我们再配置文件中有两个类型一样的 bean 是idea也会进行提示:

执行测试程序

测试结果说明了,当 byType 进行自动装配的时候,配置文件中某种类型的 Bean 必须是唯一的,不能出现多个。

2.8 Spring引入外部属性配置文件

我们都知道编写数据源的时候是需要连接数据库的信息的,例如:driver、url、username、password等信息。这些信息可以单独写到一个属性配置文件中。

新建spting_004模块

a. 创建如下类

1
2
3
4
5
6
7
8
9
10
11
@Setter
@ToString
public class MyDataSource implements DataSource {
private String driver;
private String url;
private String username;
private String password;

// --snip--

}

b. 在类路径下新建 jdbc.properties 文件,并配置信息

1
2
3
4
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spring
username=root
password=root

c. 在 spring 配置文件中引入 context 命名空间,使用 jdbc.properties 文件

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

<bean id="dataSource" class="com.muyoukule.Bean.MyDataSource">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</bean>

</beans>

d. 测试程序

1
2
3
4
5
6
@Test
public void testProperties() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-properties.xml");
MyDataSource dataSource = applicationContext.getBean("dataSource", MyDataSource.class);
System.out.println(dataSource);
}

d. 运行测试类

1
MyDataSource(driver=com.mysql.cj.jdbc.Driver, url=jdbc:mysql://localhost:3306/spring, username=权, password=root)

至此,读取 properties 配置文件中的内容就已经完成,但是在使用的时候,有些注意事项:

键值对的key为 username 引发的问题

在properties中配置键值对的时候,如果key设置为username

1
username=root

spring-properties.xml 注入该属性

1
2
3
4
5
6
7
8
<context:property-placeholder location="jdbc.properties"/>

<bean id="dataSource" class="com.muyoukule.Bean.MyDataSource">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</bean>

运行后,在控制台打印的却不是root,而是自己电脑的用户名。

出现问题的原因是 <context:property-placeholder/> 标签会加载系统的环境变量,而且环境变量的值会被优先加载,可以通过以下代码查看系统的环境变量:

1
2
3
4
public static void main(String[] args) throws Exception {
Map<String, String> env = System.getenv();
System.out.println(env);
}

大家可以自行运行,在打印出来的结果中会有一个USERNAME=XXX[自己电脑的用户名称]

两个解决方案:

  • 为 xml 文件的 <context:property-placeholder/> 标签添加 system-properties-mode: 属性并且设置为 NEVER ,表示不加载系统属性。
  • 避免使用 username 作为属性的 key
1
<context:property-placeholder location="jdbc.properties" system-properties-mode="NEVER"/>

再次运行测试类

1
MyDataSource(driver=com.mysql.cj.jdbc.Driver, url=jdbc:mysql://localhost:3306/spring, username=root, password=root)

当有多个properties配置文件需要被加载,该如何配置?

a. 调整下配置文件的内容,在resources下添加 jdbc.propertiesjdbc2.properties ,内容如下:

jdbc.properties

1
2
3
4
driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/spring
username=root
password=root

jdbc2.properties

1
username=root666

b. 修改 spring-properties.xml

1
2
3
4
5
6
7
8
<!--方式一 -->
<context:property-placeholder location="jdbc.properties,jdbc2.properties" system-properties-mode="NEVER"/>
<!--方式二-->
<context:property-placeholder location="*.properties" system-properties-mode="NEVER"/>
<!--方式三 -->
<context:property-placeholder location="classpath:*.properties" system-properties-mode="NEVER"/>
<!--方式四-->
<context:property-placeholder location="classpath*:*.properties" system-properties-mode="NEVER"/>

说明:

  • 方式一:可以实现,如果配置文件多的话,每个都需要配置
  • 方式二:*.properties 代表所有以 properties 结尾的文件都会被加载,可以解决方式一的问题,但是不标准
  • 方式三:标准的写法,classpath: 代表的是从根路径下开始查找,但是只能查询当前项目的根路径
  • 方式四:不仅可以加载当前项目还可以加载当前项目所依赖的所有项目的根路径下的 properties 配置文件

3. Bean的作用域

3.1 singleton

默认情况下,Spring 的 IoC 容器创建的 Bean 对象是单例的。

a. 创建如下类

1
2
public class SpringBean {
}

b. spring-scope.xml

1
2
3
4
5
6
7
8
<?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="sb" class="com.muyoukule.Bean.SpringBean"/>

</beans>

c. 测试

1
2
3
4
5
6
7
8
@Test
public void testScope() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");
SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb1);
SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb2);
}

d. 运行结果

1
2
com.muyoukule.Bean.SpringBean@5d066c7d
com.muyoukule.Bean.SpringBean@5d066c7d

通过测试得知:Spring的IoC容器中,默认情况下,Bean对象是单例的。

这个对象在什么时候创建的呢?

a. 为 SpringBean 提供一个无参数构造方法

1
2
3
4
5
public class SpringBean {
public SpringBean() {
System.out.println("SpringBean的无参数构造方法执行...");
}
}

b. 将测试程序中 getBean() 所在行代码注释掉,测试程序

1
2
3
4
5
6
7
8
9
@Test
public void testScope(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");

/* SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb1);
SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb2);*/
}

c. 结果

1
SpringBean的无参数构造方法执行...

通过测试得知,默认情况下,Bean对象的创建是在初始化Spring上下文的时候就完成的。

3.2 prototype

如果想让Spring的Bean对象以多例的形式存在,可以在 bean 标签中指定 scope 属性的值为:prototype,这样 Spring 会在每一次执行 getBean() 方法的时候创建Bean对象,调用几次则创建几次。

a. 修改 spring-scope.xml

1
<bean id="sb" class="com.muyoukule.Bean.SpringBean" scope="prototype"/>

b. 测试程序

1
2
3
4
5
6
7
8
9
@Test
public void testScope(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-scope.xml");

SpringBean sb1 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb1);
SpringBean sb2 = applicationContext.getBean("sb", SpringBean.class);
System.out.println(sb2);
}

c. 结果

1
2
3
4
SpringBean的无参数构造方法执行...
com.muyoukule.Bean.SpringBean@3adcc812
SpringBean的无参数构造方法执行...
com.muyoukule.Bean.SpringBean@35432107

把测试代码中的 getBean() 方法所在行代码注释掉,执行测试,结果控制台无任何信息。

默认情况下,Spring创建的 bean 对象都是单例的

介绍完 scope 属性以后,我们来思考几个问题:

  • 为什么 bean 默认为单例?
    • bean 为单例的意思是在 Spring 的 IOC 容器中只会有该类的一个对象
    • bean 对象只有一个就避免了对象的频繁创建与销毁,达到了 bean 对象的复用,性能高
  • bean 在容器中是单例的,会不会产生线程安全问题?
    • 如果对象是有状态对象,即该对象有成员变量可以用来存储数据的,
    • 因为所有请求线程共用一个 bean 对象,所以会存在线程安全问题。
    • 如果对象是无状态对象,即该对象没有成员变量没有进行数据存储的,
    • 因方法中的局部变量在方法调用完成后会被销毁,所以不会存在线程安全问题。
  • 哪些 bean 对象适合交给容器进行管理?
    • 表现层对象
    • 业务层对象
    • 数据层对象
    • 工具对象
  • 哪些 bean 对象不适合交给容器进行管理?
    • 封装实例的域对象,因为会引发线程安全问题,所以不适合。

3.3 其它scope

scope属性的值不止两个,它一共包括8个选项:

  • singleton:默认的,单例。
  • prototype:原型。每调用一次 getBean() 方法则获取一个新的Bean对象。或每次注入的时候都是新对象。
  • request:一个请求对应一个 Bean 。仅限于在WEB应用中使用
  • session:一个会话对应一个 Bean 。仅限于在WEB应用中使用
  • global session:portlet应用中专用的。如果在 Servlet 的 WEB 应用中使用 global session 的话,和 session 一个效果。(portlet和servlet都是规范。servlet运行在servlet容器中,例如Tomcat。portlet运行在portlet容器中。)
  • application:一个应用对应一个 Bean 。仅限于在WEB应用中使用。
  • websocket:一个 websocket 生命周期对应一个 Bean 。仅限于在WEB应用中使用。
  • 自定义scope:很少使用。

4. Bean的实例化方式

Spring为Bean提供了多种实例化方式,通常包括4种方式。(也就是说在 Spring 中为 Bean 对象的创建准备了多种方案,目的是:更加灵活)

  • 第一种:通过构造方法实例化
  • 第二种:通过简单工厂模式实例化
  • 第三种:通过 factory-bean 实例化
  • 第四种:通过 FactoryBean 接口实例化

4.1 通过构造方法实例化

我们之前一直使用的就是这种方式。默认情况下,会调用 Bean 的无参数构造方法。

a. 创建如下类

1
2
3
4
5
public class User {
public User() {
System.out.println("User类的无参数构造方法执行...");
}
}

b. spring.xml

1
<bean id="userBean" class="com.muyoukule.Bean.User"/>

c. 测试

1
2
3
4
5
6
@Test
public void testConstructor() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User user = applicationContext.getBean("userBean", User.class);
System.out.println(user);
}

d. 结果

1
2
User类的无参数构造方法执行...
com.muyoukule.Bean.User@5d066c7d

4.2 通过简单工厂模式实例化

a. 创建如下类

1
2
public class Vip {
}

b. 编写简单工厂模式当中的工厂类

1
2
3
4
5
public class VipFactory {
public static Vip get() {
return new Vip();
}
}

c. 在 Spring 配置文件中指定创建该 Bean 的方法(使用 factory-method 属性指定)

1
<bean id="vipBean" class="com.muyoukule.Bean.VipFactory" factory-method="get"/>

d. 编写测试程序

1
2
3
4
5
6
@Test
public void testSimpleFactory() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Vip vip = applicationContext.getBean("vipBean", Vip.class);
System.out.println(vip);
}

e. 执行结果

1
com.muyoukule.Bean.Vip@54e7df6a

4.3 通过factory-bean实例化

这种方式本质上是:通过工厂方法模式进行实例化。

a. 创建如下类

1
2
public class Order {
}

b. 定义具体工厂类,工厂类中定义实例方法

1
2
3
4
5
public class OrderFactory {
public Order get() {
return new Order();
}
}

c. 在 Spring 配置文件中指定 factory-bean 以及 factory-method

1
2
<bean id="orderFactory" class="com.muyoukule.Bean.OrderFactory"/>
<bean id="orderBean" factory-bean="orderFactory" factory-method="get"/>

d. 编写测试程序

1
2
3
4
5
6
@Test
public void testSelfFactoryBean() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Order orderBean = applicationContext.getBean("orderBean", Order.class);
System.out.println(orderBean);
}

e. 执行结果

1
com.muyoukule.Bean.Order@32c4e8b2

4.4 通过FactoryBean接口实例化

以上的第三种方式中,factory-bean 是我们自定义的,factory-method 也是我们自己定义的。

在Spring中,当你编写的类直接实现 FactoryBean 接口之后,factory-bean 不需要指定了,factory-method 也不需要指定了。

factory-bean 会自动指向实现 FactoryBean 接口的类,factory-method 会自动指向 getObject() 方法。

a. 创建如下类

1
2
public class Person {
}

b. 编写一个类实现 FactoryBean 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PersonFactoryBean implements FactoryBean<Person> {

@Override
public Person getObject() throws Exception {
return new Person();
}

@Override
public Class<?> getObjectType() {
return null;
}

@Override
public boolean isSingleton() {
// true表示单例
// false表示原型
return true;
}
}

c. 在 Spring 配置文件中配置 FactoryBean

1
<bean id="personBean" class="com.muyoukule.Bean.PersonFactoryBean"/>

d. 测试程序

1
2
3
4
5
6
7
8
@Test
public void testFactoryBean() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Person personBean = applicationContext.getBean("personBean", Person.class);
System.out.println(personBean);
Person personBean2 = applicationContext.getBean("personBean", Person.class);
System.out.println(personBean2);
}

e. 执行结果

1
2
com.muyoukule.Bean.Person@4f0100a7
com.muyoukule.Bean.Person@4f0100a7

4.5 BeanFactory和FactoryBean的区别

BeanFactory

Spring IoC 容器的顶级对象,BeanFactory 被翻译为 “Bean工厂” ,在 Spring 的 IoC 容器中,“Bean工厂” 负责创建 Bean 对象。

BeanFactory 是工厂。

FactoryBean

FactoryBean:它是一个 Bean,是一个能够辅助Spring实例化其它 Bean 对象的一个 Bean 。

在Spring中,Bean可以分为两类:

  • 第一类:普通Bean
  • 第二类:工厂 Bean(记住:工厂 Bean 也是一种 Bean,只不过这种 Bean 比较特殊,它可以辅助 Spring 实例化其它 Bean 对象。)

4.6 注入自定义Date

我们前面说过,java.util.Date 在 Spring 中被当做简单类型,简单类型在注入的时候可以直接使用 value 属性或 value 标签来完成。但我们之前已经测试过了,对于Date类型来说,采用 value 属性或 value 标签赋值的时候,对日期字符串的格式要求非常严格,必须是这种格式的:Mon Oct 10 14:30:26 CST 2022。其他格式是不会被识别的。

a. 创建如下类

1
2
3
4
5
@Setter
@ToString
public class Student {
private Date birth;
}

b. spring-data.xml

1
2
3
<bean id="studentBean" class="com.muyoukule.Bean.Student">
<property name="birth" value="Mon Oct 10 14:30:26 CST 2002"/>
</bean>

c. 测试程序

1
2
3
4
5
6
@Test
public void testDate() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-data.xml");
Student studentBean = applicationContext.getBean("studentBean", Student.class);
System.out.println(studentBean);
}

d. 执行结果

1
Student(birth=Fri Oct 11 04:30:26 CST 2002)

如果把日期格式修改一下:

1
2
3
<bean id="studentBean" class="com.muyoukule.Bean.Student">
<property name="birth" value="2002-10-10"/>
</bean>

执行结果

这种情况下,我们就可以使用 FactoryBean 来完成这个操作。

a. 编写 DateFactoryBean 实现 FactoryBean 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DateFactoryBean implements FactoryBean<Date> {

// 定义属性接收日期字符串
private String date;

// 通过构造方法给日期字符串属性赋值
public DateFactoryBean(String date) {
this.date = date;
}

@Override
public Date getObject() throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.parse(this.date);
}

@Override
public Class<?> getObjectType() {
return null;
}
}

b. 编写spring配置文件

1
2
3
4
5
6
<bean id="dateBean" class="com.muyoukule.Bean.DateFactoryBean">
<constructor-arg name="date" value="1999-10-11"/>
</bean>
<bean id="studentBean" class="com.muyoukule.Bean.Student">
<property name="birth" ref="dateBean"/>
</bean>

c. 执行测试程序

1
Student(birth=Mon Oct 11 00:00:00 CST 1999)

5. Bean的生命周期

5.1 什么是Bean的生命周期

Spring其实就是一个管理Bean对象的工厂。它负责对象的创建,对象的销毁等。

所谓的生命周期就是:对象从创建开始到最终销毁的整个过程。

什么时候创建Bean对象?

创建Bean对象的前后会调用什么方法?

Bean对象什么时候销毁?

Bean对象的销毁前后调用什么方法?

5.2 为什么要知道Bean的生命周期

其实生命周期的本质是:在哪个时间节点上调用了哪个类的哪个方法。

我们需要充分的了解在这个生命线上,都有哪些特殊的时间节点。

只有我们知道了特殊的时间节点都在哪,到时我们才可以确定代码写到哪。

我们可能需要在某个特殊的时间点上执行一段特定的代码,这段代码就可以放到这个节点上。当生命线走到这里的时候,自然会被调用。

5.3 Bean的生命周期之5步

Bean 生命周期的管理,可以参考Spring的源码:AbstractAutowireCapableBeanFactory 类的 doCreateBean() 方法

Bean 生命周期可以粗略的划分为五大步:

  • 第一步:实例化Bean
  • 第二步:Bean属性赋值
  • 第三步:初始化Bean
  • 第四步:使用Bean
  • 第五步:销毁Bean

编写测试程序:

a. 创建如下类

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

public User() {
System.out.println("1.实例化Bean");
}

public void setName(String name) {
this.name = name;
System.out.println("2.Bean属性赋值");
}

public void initBean() {
System.out.println("3.初始化Bean");
}

public void destroyBean() {
System.out.println("5.销毁Bean");
}

}

b. spring.xml

1
2
3
4
<!--init-method属性指定初始化方法。destroy-method属性指定销毁方法。-->
<bean id="userBean" class="com.muyoukule.Bean.User" init-method="initBean" destroy-method="destroyBean">
<property name="name" value="zhangsan"/>
</bean>

c. 测试程序

1
2
3
4
5
6
7
8
9
@Test
public void testLifecycle() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User userBean = applicationContext.getBean("userBean", User.class);
System.out.println("4.使用Bean");
// 只有正常关闭spring容器才会执行销毁方法
ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
context.close();
}

d. 执行结果

1
2
3
4
5
1.实例化Bean
2.Bean属性赋值
3.初始化Bean
4.使用Bean
5.销毁Bean

需要注意的:

  • 第一:只有正常关闭 spring 容器,bean 的销毁方法才会被调用。
  • 第二:ClassPathXmlApplicationContext 类才有 close() 方法。
  • 第三:配置文件中的 init-method 指定初始化方法。 destroy-method 指定销毁方法。

5.4 Bean生命周期之7步

在以上的5步中,第3步是初始化 Bean ,如果你还想在初始化前和初始化后添加代码,可以加入“ Bean 后处理器”。

a. 编写一个类实现 BeanPostProcessor 类,并且重写 beforeafter 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LogBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Bean后处理器的before方法执行,即将开始初始化");
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Bean后处理器的after方法执行,已完成初始化");
return bean;
}
}

b. 在 spring.xml 文件中配置“ Bean 后处理器”:

1
2
3
4
5
6
7
<!--init-method属性指定初始化方法。destroy-method属性指定销毁方法。-->
<bean id="userBean" class="com.muyoukule.Bean.User" init-method="initBean" destroy-method="destroyBean">
<property name="name" value="zhangsan"/>
</bean>

<!--配置Bean后处理器。这个后处理器将作用于当前配置文件中所有的bean。-->
<bean class="com.muyoukule.Bean.LogBeanPostProcessor"/>

一定要注意:在 spring.xml 文件中配置的 Bean 后处理器将作用于当前配置文件中所有的 Bean 。

c. 执行测试程序

1
2
3
4
5
6
7
1.实例化Bean
2.Bean属性赋值
Bean后处理器的before方法执行,即将开始初始化
3.初始化Bean
Bean后处理器的after方法执行,已完成初始化
4.使用Bean
5.销毁Bean

5.5 Bean生命周期之10步

如果根据源码跟踪,可以划分更细粒度的步骤,10步:

上图中检查 Bean 是否实现了 Aware 的相关接口是什么意思?

Aware 相关的接口包括:BeanNameAwareBeanClassLoaderAwareBeanFactoryAware

  • 当Bean实现了 BeanNameAware ,Spring 会将 Bean 的名字传递给 Bean。
  • 当Bean实现了 BeanClassLoaderAware ,Spring 会将加载该 Bean 的类加载器传递给 Bean。
  • 当Bean实现了 BeanFactoryAware ,Spring会将 Bean 工厂对象传递给 Bean。

测试以上10步,可以让 User类实现5个接口,并实现所有方法:

  • BeanNameAware
  • BeanClassLoaderAware
  • BeanFactoryAware
  • InitializingBean
  • DisposableBean

a. 代码如下:

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
public class User implements BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean, DisposableBean {
private String name;

public User() {
System.out.println("1.实例化Bean");
}

public void setName(String name) {
this.name = name;
System.out.println("2.Bean属性赋值");
}

public void initBean() {
System.out.println("6.初始化Bean");
}

public void destroyBean() {
System.out.println("10.销毁Bean");
}

@Override
public void setBeanClassLoader(ClassLoader classLoader) {
System.out.println("3.类加载器:" + classLoader);
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("3.Bean工厂:" + beanFactory);
}

@Override
public void setBeanName(String name) {
System.out.println("3.bean名字:" + name);
}

@Override
public void destroy() throws Exception {
System.out.println("9.DisposableBean destroy");
}

@Override
public void afterPropertiesSet() throws Exception {
System.out.println("5.afterPropertiesSet执行");
}
}

b. LogBeanPostProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
public class LogBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("4.Bean后处理器的before方法执行,即将开始初始化");
return bean;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("7.Bean后处理器的after方法执行,已完成初始化");
return bean;
}
}

c. 测试

1
2
3
4
5
6
7
8
9
@Test
public void testLifecycle() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
User userBean = applicationContext.getBean("userBean", User.class);
System.out.println("8.使用Bean");
// 只有正常关闭spring容器才会执行销毁方法
ClassPathXmlApplicationContext context = (ClassPathXmlApplicationContext) applicationContext;
context.close();
}

d. 执行结果

1
2
3
4
5
6
7
8
9
10
11
12
1.实例化Bean
2.Bean属性赋值
3.bean名字:userBean
3.类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
3.Bean工厂:org.springframework.beans.factory.support.DefaultListableBeanFactory@1bae316d: defining beans [userBean,com.muyoukule.Bean.LogBeanPostProcessor#0]; root of factory hierarchy
4.Bean后处理器的before方法执行,即将开始初始化
5.afterPropertiesSet执行
6.初始化Bean
7.Bean后处理器的after方法执行,已完成初始化
8.使用Bean
9.DisposableBean destroy
10.销毁Bean

通过测试可以看出来:

  • InitializingBean 的方法早于 init-method 的执行。
  • DisposableBean 的方法早于 destroy-method 的执行。

5.6 Bean的作用域不同,管理方式不同

Spring 根据 Bean 的作用域来选择管理方式。

  • 对于 singleton 作用域的 Bean,Spring 能够精确地知道该 Bean 何时被创建,何时初始化完成,以及何时被销毁;
  • 而对于 prototype 作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给客户端代码管理,Spring 容器将不再跟踪其生命周期。

a. 把之前User类的spring.xml文件中的配置 scope 设置为 prototype

1
2
3
4
5
<!--init-method属性指定初始化方法。destroy-method属性指定销毁方法。-->
<bean id="userBean" class="com.muyoukule.Bean.User" init-method="initBean" destroy-method="destroyBean"
scope="prototype">
<property name="name" value="zhangsan"/>
</bean>

b. 执行测试程序

1
2
3
4
5
6
7
8
9
10
1.实例化Bean
2.Bean属性赋值
3.bean名字:userBean
3.类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader@63947c6b
3.Bean工厂:org.springframework.beans.factory.support.DefaultListableBeanFactory@1bae316d: defining beans [userBean,com.muyoukule.Bean.LogBeanPostProcessor#0]; root of factory hierarchy
4.Bean后处理器的before方法执行,即将开始初始化
5.afterPropertiesSet执行
6.初始化Bean
7.Bean后处理器的after方法执行,已完成初始化
8.使用Bean

通过测试一目了然。只执行了前8步,第9和10都没有执行。

5.7 自己new的对象如何让Spring管理

有些时候可能会遇到这样的需求,某个java对象是我们自己new的,然后我们希望这个对象被Spring容器管理,怎么实现?

a. 创建如下类

1
2
public class Student {
}

b. 测试

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testBeanRegister() {
// 自己new的对象
Student student = new Student();
System.out.println(student);
// 创建 默认可列表BeanFactory 对象
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 注册Bean
factory.registerSingleton("studentBean", student);
// 从spring容器中获取bean
Student studentBean = factory.getBean("studentBean", Student.class);
System.out.println(studentBean);
}

c. 结果

1
2
com.muyoukule.Bean.Student@6bdf28bb
com.muyoukule.Bean.Student@6bdf28bb

6. Bean的循环依赖问题

6.1 什么是Bean的循环依赖

A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。

比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。

Husband

1
2
3
4
public class Husband {
private String name;
private Wife wife;
}

Wife

1
2
3
4
public class Wife {
private String name;
private Husband husband;
}

6.2 singleton下的set注入产生的循环依赖

我们来编写程序,测试一下在 singleton + setter 的模式下产生的循环依赖,Spring是否能够解决?

a. 给上面的类添加方法

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
@Getter
@Setter
public class Husband {
private String name;
private Wife wife;

// toString()方法重写时需要注意:不能直接输出wife,输出wife.getName()。要不然会出现递归导致的栈内存溢出错误。
@Override
public String toString() {
return "Husband{" +
"name='" + name + '\'' +
", wife=" + wife.getName() +
'}';
}
}

@Getter
@Setter
public class Wife {
private String name;
private Husband husband;

// toString()方法重写时需要注意:不能直接输出husband,输出husband.getName()。要不然会出现递归导致的栈内存溢出错误。
@Override
public String toString() {
return "Wife{" +
"name='" + name + '\'' +
", husband=" + husband.getName() +
'}';
}
}

b. spring.xml

1
2
3
4
5
6
7
8
<bean id="husbandBean" class="com.muyoukule.Bean.Husband" scope="singleton">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.muyoukule.Bean.Wife" scope="singleton">
<property name="name" value="小花"/>
<property name="husband" ref="husbandBean"/>
</bean>

c. 测试

1
2
3
4
5
6
7
8
@Test
public void testSingletonAndSet() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Husband husbandBean = applicationContext.getBean("husbandBean", Husband.class);
Wife wifeBean = applicationContext.getBean("wifeBean", Wife.class);
System.out.println(husbandBean);
System.out.println(wifeBean);
}

d. 结果

1
2
Husband{name='张三', wife=小花}
Wife{name='小花', husband=张三}

通过测试得知:在 singleton + set 注入的情况下,循环依赖是没有问题的。Spring 可以解决这个问题。

6.3 prototype下的set注入产生的循环依赖

我们再来测试一下:prototype + set 注入的方式下,循环依赖会不会出现问题?

a. 修改spring.xml

1
2
3
4
5
6
7
8
<bean id="husbandBean" class="com.muyoukule.Bean.Husband" scope="prototype">
<property name="name" value="张三"/>
<property name="wife" ref="wifeBean"/>
</bean>
<bean id="wifeBean" class="com.muyoukule.Bean.Wife" scope="prototype">
<property name="name" value="小花"/>
<property name="husband" ref="husbandBean"/>
</bean>

b. 测试

翻译为:创建名为“ husbandBean ”的 bean 时出错:请求的 bean 当前正在创建中:是否存在无法解析的循环引用?

通过测试得知,当循环依赖的所有 Bean 的 scope="prototype" 的时候,产生的循环依赖,Spring 是无法解决的,会出现 BeanCurrentlyInCreationException 异常。

大家可以测试一下,以上两个 Bean,如果其中一个是 singleton ,另一个是 prototype ,是没有问题的。

为什么两个Bean都是 prototype 时会出错呢?

6.4 singleton下的构造注入产生的循环依赖

我们再来测试一下 singleton + 构造注入 的方式下,spring是否能够解决这种循环依赖。

a. 创建如下类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Getter
@AllArgsConstructor
@ToString
public class Husband {
private String name;
private Wife wife;

}

@Getter
@AllArgsConstructor
@ToString
public class Wife {
private String name;
private Husband husband;
}

b. 修改 spring.xml

1
2
3
4
5
6
7
8
<bean id="hBean" class="com.muyoukule.Bean.Husband" scope="singleton">
<constructor-arg name="name" value="张三"/>
<constructor-arg name="wife" ref="wBean"/>
</bean>
<bean id="wBean" class="com.muyoukule.Bean.Wife" scope="singleton">
<constructor-arg name="name" value="小花"/>
<constructor-arg name="husband" ref="hBean"/>
</bean>

c. 测试

1
2
3
4
5
6
7
8
@Test
public void testSingletonAndConstructor() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
Husband hBean = applicationContext.getBean("hBean", Husband.class);
Wife wBean = applicationContext.getBean("wBean", Wife.class);
System.out.println(hBean);
System.out.println(wBean);
}

d. 结果

和上一个测试结果相同,都是提示产生了循环依赖,并且Spring是无法解决这种循环依赖的。这是通过构造方法注入导致的:因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成。

6.5 Spring解决循环依赖的机理

Spring 为什么可以解决 set + singleton 模式下循环依赖?

根本的原因在于:这种方式可以做到将“实例化 Bean ”和“给 Bean 属性赋值”这两个动作分开去完成。

  • 实例化 Bean 的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该 Bean 对象“曝光”给外界。

  • 给 Bean 属性赋值的时候:调用 setter 方法来完成。

两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成。

也就是说,Bean 都是单例的,我们可以先把所有的单例 Bean 实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例Bean 全部实例化完成之后,以后我们再慢慢的调用 setter 方法给属性赋值。这样就解决了循环依赖的问题。

那么在Spring框架底层源码级别上是如何实现的呢?请看:

在以上类中包含三个重要的属性:

1
2
//Cache of singleton objects: bean name to bean instance
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);

Cache of singleton objects: bean name to bean instance. 单例对象的缓存:key存储 bean 名称,value 存储 Bean 对象【一级缓存】

1
2
//Cache of early singleton objects: bean name to bean instance
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);

Cache of early singleton objects: bean name to bean instance. 早期单例对象的缓存:key 存储 bean 名称,value 存储早期的 Bean 对象【二级缓存】

1
2
//Cache of singleton factories: bean name to ObjectFactory
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);

Cache of singleton factories: bean name to ObjectFactory. 单例工厂缓存:key 存储 bean 名称,value 存储该 Bean 对应的 ObjectFactory 对象【三级缓存】

这三个缓存其实本质上是三个Map集合。

我们再来看,在该类中有这样一个方法 addSingletonFactory() ,这个方法的作用是:将创建 Bean 对象的 ObjectFactory 对象提前曝光。

再分析下面的源码:

从源码中可以看到,spring 会先从一级缓存中获取 Bean,如果获取不到,则从二级缓存中获取 Bean,如果二级缓存还是获取不到,则从三级缓存中获取之前曝光的 ObjectFactory 对象,通过 ObjectFactory 对象获取 Bean 实例,这样就解决了循环依赖的问题。

总结

Spring 只能解决 setter 方法注入的单例 bean 之间的循环依赖。ClassA 依赖 ClassB ,ClassB 又依赖ClassA ,形成依赖闭环。Spring 在创建 ClassA 对象后,不需要等给属性赋值,直接将其曝光到 bean 缓存当中。在解析 ClassA 的属性时,又发现依赖于 ClassB ,再次去获取 ClassB,当解析 ClassB 的属性时,又发现需要 ClassA 的属性,但此时的 ClassA 已经被提前曝光加入了正在创建的 bean 的缓存中,则无需创建新的的 ClassA 的实例,直接从缓存中获取即可。从而解决循环依赖问题。