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 > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-context</artifactId > <version > 5.3.27</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.13.2</version > <scope > test</scope > </dependency > </dependencies >
当加入 spring context
的依赖之后,会关联引入其他依赖:
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 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) { 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) { ApplicationContext ctx = new ClassPathXmlApplicationContext ("applicationContext.xml" ); User userBean = ctx.getBean("userBean" , User.class); userBean.hello(); } }
g. 运行 main()
方法
至此,Spring的IOC入门案例已经完成。
1.2 IOC入门案例详解
Bean 标签的 id 属性可以重复吗?
新建一个 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 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 (String name) { System.out.println("User的有参数构造方法执行" ); } public void hello () { System.out.println("hello userBean...." ); } }
运行测试程序
通过测试得知:spring 是通过调用类的无参数构造方法来创建对象的,所以要想让 spring 给你创建对象,必须保证无参数构造方法是存在的。
Spring是如何创建对象的呢?原理是什么?
1 2 3 4 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) { 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) { 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 容器的超级接口。ApplicationContext
是 BeanFactory
的子接口。
1.3 Spring启用Log4j2日志框架 从Spring5之后,Spring框架支持集成的日志框架是 Log4j2
。如何启用日志框架:
a. 引入 Log4j2
的依赖
1 2 3 4 5 6 7 8 9 10 11 <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 > <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 控制反转
2.2 依赖注入 依赖注入实现了控制反转的思想。Spring通过依赖注入的方式来完成Bean管理的。
Bean管理说的是:Bean
对象的创建,以及Bean对象中属性的赋值(或者叫做 Bean
对象之间关系的维护)。
依赖注入:
依赖指的是对象和对象之间的关联关系。
注入指的是一种数据传递行为,通过注入行为来让对象和对象产生关系。
依赖注入常见的实现方式包括两种:
新建模块: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; 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" /> <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; 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" > <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" > <constructor-arg index ="0" ref ="orderDaoBean" /> <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" /> <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 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" > <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. 运行测试程序
需要特别注意:如果给简单类型赋值,使用 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 { public static boolean isSimpleProperty (Class<?> type) { Assert.notNull(type, "'type' must not be null" ); return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.getComponentType())); } 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)); } }
通过源码分析得知,简单类型包括:
基本数据类型、基本数据类型对应的包装类、String
或其他的 CharSequence
子类、Number
子类、Date
子类、Enum
子类、URI
、URL
、Temporal
子类、Locale
、Class
、另外还包括以上简单值类型对应的数组类型。
测试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" > <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
当做简单类型的话,日期字符串格式不能随便写。格式必须符合 Date
的 toString()
方法格式。显然这就比较鸡肋了。如果我们提供一个这样的日期字符串: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 ="张三" /> <property name ="clazz" ref ="clazzBean" /> <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 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 > <value > 110</value > <value > 110</value > <value > 120</value > <value > 120</value > <value > 119</value > <value > 119</value > </set > </property > <property name ="addrs" > <map > <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:
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. 运行测试程序
注入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. 运行测试类
2.3.8 注入的值中含有特殊符号 XML中有5个特殊字符,分别是:< 、> 、’ 、” 、&
以上5个特殊符号在 XML 中会被特殊对待,会被当做 XML 语法的一部分进行解析,如果这些特殊符号直接出现在注入的字符串当中,会报错。
解决方案包括两种:
第一种:特殊符号使用转义字符代替。
第二种:将含有特殊符号的字符串放到:<![CDATA[]]> 当中。因为放在CDATA区中的数据不会被XML文件解析器解析。
5个特殊字符对应的转义字符分别是:
特殊字符
转义字符
>
>
<
<
‘
'
“
"
&
&
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 < 3" /> </bean >
使用CDATA方式
1 2 3 4 5 6 <bean id ="mathBean" class ="com.muyoukule.Bean.Math" > <property name ="result" > <value > <![CDATA[2 < 3]]></value > </property > </bean >
c. 编写测试类
1 2 3 4 5 6 7 @Test public void testSpecial () { ApplicationContext applicationContext = new ClassPathXmlApplicationContext ("spring-special.xml" ); Math mathBean = applicationContext.getBean("mathBean" , Math.class); System.out.println(mathBean); }
d. 运行测试类
使用 CDATA 时,不能使用 value
属性,只能使用 value
标签。
2.4 p命名空间注入 目的:简化配置。
p命名空间实际上是对 set
注入的简化。
使用p命名空间注入的前提条件包括两个:
新建spting_003模块
a. 创建如下类
1 2 3 4 5 6 7 @Setter @ToString public class Customer { 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:_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; public void setAaa (UserDao aaa) { this .aaa = aaa; } public void save () { System.out.println("UserService..." ); aaa.insert(); } }
b. spring-autowire.xml
1 2 3 <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; public void setDao (UserDao aaa) { this .aaa = aaa; } }
再执行测试程序
通过测试得知,aaa 属性并没有赋值成功。也就是并没有装配成功。
将 spring 配置文件修改以下:
1 2 <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 <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 <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; }
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
在 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.properties
,jdbc2.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
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" ); }
c. 结果
通过测试得知,默认情况下,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. 创建如下类
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. 创建如下类
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. 创建如下类
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 () { 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 <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" ); 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
类,并且重写 before
和 after
方法:
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 <bean id ="userBean" class ="com.muyoukule.Bean.User" init-method ="initBean" destroy-method ="destroyBean" > <property name ="name" value ="zhangsan" /> </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 相关的接口包括:BeanNameAware
、BeanClassLoaderAware
、BeanFactoryAware
当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" ); 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 <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 () { Student student = new Student (); System.out.println(student); DefaultListableBeanFactory factory = new DefaultListableBeanFactory (); factory.registerSingleton("studentBean" , student); 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; @Override public String toString () { return "Husband{" + "name='" + name + '\'' + ", wife=" + wife.getName() + '}' ; } } @Getter @Setter public class Wife { private String name; private Husband husband; @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
方法给属性赋值。这样就解决了循环依赖的问题。
那么在Spring框架底层源码级别上是如何实现的呢?请看:
在以上类中包含三个重要的属性:
1 2 private final Map<String, Object> singletonObjects = new ConcurrentHashMap (256 );
Cache of singleton objects: bean name to bean instance.
单例对象的缓存:key存储 bean 名称,value 存储 Bean 对象【一级缓存】
1 2 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 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 的实例,直接从缓存中获取即可。从而解决循环依赖问题。