Spring之旅
- Spring是一个轻量级的开源Java框架
- Spring的优势就是分层架构
- Spring的核心就是控制反转(IOC)和面向切面(AOP)
-
JavaEE开发分为三层结构:
-
Web层 –>SpringMVC
-
业务层 –>Bean管理(IOC)
-
持久层 –>Spring的JDBC模板、ORM模板用于整合其他持久层框架
-
首先我们要引入Spring提供的jar包:
Spring核心之装配Bean
Spring通过装配Bean对象来完成各个应用之间的协同合作,这也是依赖注入的本质。
而依赖注入即是我们前文提到的IOC
控制反转的思想–>通过将应用对象装配进Spring Bean
中,即由Spring管理对象的依赖关系。
创建Spring的配置
Spring是一个基于容器的框架,我们需要通过配置告诉Spring去加载哪些Bean和如果装配这些Bean。
- 配置Spring的方式有两种:
- 在XML文件中声明Bean
- 通过注解配置Spring
首先以下是一个基本的Spring XML配置:
1 2 3 4 5 6 | <?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> |
在<beans>
元素内,可以配置所有的Spring的配置信息。而beans
不是唯一的Spring命名空间,Spring常用的命名空间有:
* aop: 为声明切面以及将@AspectJ注解的类代理为Spring切面提供了配置元素
* beans: 支持声明bean和装配bean,是Spring最基本的命名空间
* context: 为配置Spring应用上下文气功配置元素,包括自动检测和自动装配Bean、注入非Spring直接管理的对象
* jms: 为声明小气驱动的POJO提供配置元素
* mvc: 启用SpringMVC,例如面向注解的控制器、视图解析器和拦截器
* tx:提供声明式事务配置
声明一个简单的Bean
-
创建一个
User.java
接口1 2 3 4 5 6 7 8 9 10
package demo1; public class User { private int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
-
创建Spring的配置文件
spring.xml
,并将User.java
交给Spring管理
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="user" class="demo1.User"/> </beans> |
-
解释:
分析以上案例:我们首先创建一个JavaBean对象
User.java
,然后将这个JavaBean对象交给Spring管理—>即在Spring配置文件中注入。那么,Spring是怎么实例化这个名字叫user
的Bean的呢? -
改进上述代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package demo1; public class User { public User(){ System.out.println("这是无参构造..."); } private int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } } |
即我们手动创建一个无参构造函数,结果如下:
解释:
可以看到,当我们手动创建一个无参构造函数时,定义在无参构造函数中的语句就会打印出来。原因是Spring实例化Bean采用了User user = new User()
的方式,并且会使用默认的无参构造函数进行实例化,而在Java中无参构造函数会被系统自动创建(在没有其他构造函数的情况下)。
通过构造器注入
通过以上案例,我们发现,Spring默认会使用JavaBean的无参构造函数进行注入,其实Spring还提供了一种注入方式:构造器注入。
改变Spring配置文件spring.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?xml version="1.0" encoding="utf-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 注入user(默认使用无参构造) --> <bean id="user" class="demo1.User"/> <!-- 使用Spring的构造方法 --> <bean id="user2" class="demo1.User"> <constructor-arg value="12"/> </bean> </beans> |
如上所示:在<bean>
中使用<constructor-arg>
元素来告诉Spring额外的信息,但是我们直接这样写是会报错的:
通过IDEA的报错提示我们发现解决办法是要在User.java
中创建一个带参构造函数,所以我们创建如下带参构造函数:
为了更直观的展示,我们改进上述代码:
User.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | package demo1; public class User { public User() { System.out.println("这是无参构造..."); } public User(int age) { this.age = age; System.out.println("打印age的值:" + age); System.out.println("这是一个带参构造函数"); } private int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } } |
spring.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?xml version="1.0" encoding="utf-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 注入user(默认使用无参构造) --> <bean id="user" class="demo1.User"/> <!-- 使用Spring的构造方法 --> <bean id="user2" class="demo1.User"> <constructor-arg value="12"/> </bean> </beans> |
打印结果:
综上
我们发现Spring会通过JavaBean的默认无参构造来实例化对象,并提供一种特殊的构造方法:构造器
来实例化Bean对象(但其实是使用了Bean对象的带参构造函数),构造器
的特点就是按需实例化
Spring支持实例化对象时提供额外的信息来覆盖本身定义的数据。
构造器另一种用法—>注入对象引用
spring.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <?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"> <!-- 注入user(默认使用无参构造) --> <bean id="user" class="demo1.User"/> <!-- 使用Spring的构造方法 --> <bean id="user2" class="demo1.User"> <constructor-arg value="12"/> </bean> <bean id="user3" class="demo1.User"> <constructor-arg value="joke"/> <constructor-arg ref="user2"/> </bean> </beans> |
在User.java
添加:
1 2 3 | public User(String name, User user){ System.out.println("打印name的值和age的值:" + name + "," + user.getAge()); } |
打印结果:
总结
同上上面的构造器注入方式我们发现,使用构造器注入,其实是使用JavaBean中的构造方法,而我们在构造器<constructor-arg>
中注入什么值,那么在Bean对象带参构造函数中的参数列表中就应该用什么值接收,比如我们,传入的是一个引用对象,那么就应该用对象最后接收参数。
通过工厂方法创建Bean
一般来说,单例类的实例只能通过静态工厂方法来创建。Spring支持通过<bean>
元素的factory-method
属性来装配工厂创建的Bean。
以下是一个典型的单例类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | package demo1; public class Case { private Case() { } //延迟加载实例 private static class CaseSingleHolder{ static Case instance = new Case(); } //返回实例 public static Case getInstance(){ return CaseSingleHolder.instance; } } |
以上使用了单例模式中一种懒加载单例模式,即在调用的时候才创建实例(故称为懒加载)。
以上懒加载特点就是: 1.构造函数私有 2. 方法静态
Java中单例模式具有以下特点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。 —> 加载实例
- 单例类必须给所有其他对象提供这一实例。 —> 返回实例对象
在spring.xml
中添加如下:
1 | <bean id="case" class="demo1.Case" factory-method="getInstance"/> |
注:这样应用的场景是我们不想在加载spring.xml
时就实例化Bean,而是调用factory-method
所指定的方法时,才开始真正的实例化Bean。
使用要静态工厂创建Bean要注意:这里的class
属性并不是指定Bean实例的实现类,而是静态工厂类。因为Spring需要知道是用哪个工厂承诺噶来创建Bean的实例。其次factory-method
指定的是静态工厂方法名(必须是静态的)。
如下:
运行结果(注意:此时我们测试的是run2()
方法):
发现:此时我们没有调用run1()
方法,但是两次调用run2()
方法都已经实例化了名字是user
的Bean,但是,没有调用getBean()
实例化名字是case
的Bean,Spring就不会实例化该Bean。这样正印证了<factory-method>
的特点:只有在调用<factory-method>
指定的方法时才开始实例化Bean,而不是加载spring.xml
时就实例化。
Bean的作用域
所有的Spring默认都是单例,当容器实例化一个Bean时,无论是通过装配(<constructor-arg>
),还是通过getBean()
(调用默认的无参构造),都会返回Bean的同一个实例。那么如何覆盖Spring的默认单例配置呢?
通过将scope="property"
即可,如下:
1 | <bean id="user" class="demo1.User" scope="prototype"/> |
首先我们仍用上面的例子,但让其实例化两次Bean对象,如下:
spring.xml
1 | <bean id="user" class="demo1.User"/> |
Test测试类
1 2 3 4 5 6 7 8 9 | @Test public void run() { //加载Spring上下文 ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml"); User user = (User) ac.getBean("user"); User user2 = (User) ac.getBean("user"); System.out.println(user); System.out.println(user2); } |
打印结果发现:
两次打印的结果都相同,那么就证实了Spring默认实例化Bean都是采用的单例模式。阻止了默认单例配置后的效果如下:
spring.xml
1 | <bean id="user" class="demo1.User" scope="prototype"/> |
Test测试类
1 2 3 4 5 6 7 8 9 | @Test public void run() { //加载Spring上下文 ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml"); User user = (User) ac.getBean("user"); User user2 = (User) ac.getBean("user"); System.out.println(user); System.out.println(user2); } |
打印结果:
明显发现两次打印的地址值不同。
除了以上prototype
作用域,Spring Bean还有其他几类作用域:
- singleton: 在每一个Spring容器中,一个Bean定义只有一个对象实例(默认)。
- prototype: 允许Bean的定义可以被实例化任意次(每次调用都创建一个实例)。
- request: 在一次HTTP请求中,每个Bean定义对应一个实例。该作用域尽在基于Web的Spring上下文中有效。
- session: 在一次HTTP Session请求中,每个Bean定义对应一个实例。该作用域仅在Portlet上下文中有效。
初始化和销毁Bean
Spring提供了Bean声明周期的钩子方法,用来在Bean初始化和销毁前执行。
- 初始化:
init-method
—>初始化后调用- 销毁:
destory-method
—>销毁前调用
- 销毁:
举例:
spring.xml
1 2 3 4 5 6 7 8 9 | <?xml version="1.0" encoding="utf-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 注入user(默认使用无参构造) --> <bean id="user" class="demo1.User" init-method="turnOn" destroy-method="turnOff"/> </beans> |
User.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package demo1; public class User { public User() { } public void turnOn(){ System.out.println("初始化Bean..."); } public void turnOff(){ System.out.println("销毁Bean..."); } private int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } |
Test测试类
1 2 3 4 5 6 7 8 9 | @Test public void run() { //加载Spring上下文 ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml"); User user = (User) ac.getBean("user"); System.out.println(user); //手动销毁Bean ((ClassPathXmlApplicationContext) ac).close(); } |
输出结果:
拓展: 上面的案例都是基于Bean
是单例模式的基础上,那么我们使用多例模式的情况会是怎样呢?
首先修改spring.xml
1 | <bean id="user" class="demo1.User" scope="prototype" init-method="turnOn" destroy-method="turnOff"/> |
然后我们观察运行结果:
源码分析
Spring已经调用了销毁Bean的方法,但是此时并没有执行我们定义的销毁方法turnOff()
,这是为什么呢?
那么我们看一下Spring中scope
属性的源码:
可以看到上面注解的含义大概就是说这个destory-method
方法只是对于Spring默认的单例(singletons
)而言的,而当我们定义为多例模式(prototype
)时,此时该Bean的声明周期(lifycycle
)将不再进行此方法。也就是说:当我们将Bean
定义为多例模式时,当此Bean
被实例化之后,Spring的IOC容器将不再对此Bean的声明周期进行管理了,也就不会再执行销毁方法。此时Spring对该Bean
的管理仅是在执行new
对象的操作。
2.3.1 默认的init-method和destory-method
如果Spring的上下文中出现较多的Bean需要同一个初始化、销毁方法。那么我们可以定义一个全局的方法:
在<beans>
中定义:
default-init-method="turnOn"
default-destory-method="turnOff"
注入Bean属性
通常JavaBean的属性是私有的,同时拥有一组存取器方法,以setXxx()
和getXxx()
形式存在,而Spring就可以借助setXxx()
方法里配置属性的值,以实现setter
方法注入,请看下面。
spring.xml
1 2 3 4 5 6 7 8 9 10 11 | <?xml version="1.0" encoding="utf-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 注入user(默认使用无参构造) --> <bean id="user" class="demo1.User"> <property name="age" value="18"/> </bean> </beans> |
User.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package demo1; public class User { public User() { } public User(int age) { this.age = age; System.out.println("打印age的值:" + age); } public User(String name, User user){ System.out.println("打印name的值和age的值:" + name + "," + user.getAge()); } private int age; public int getAge() { return age; } public void setAge(int age) { System.out.println("执行setAge()..."); this.age = age; } private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } |
Test测试方法
1 2 3 4 5 6 7 | @Test public void run() { //加载Spring上下文 ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml"); User user = (User) ac.getBean("user"); System.out.println(user.getAge()); } |
打印结果
可以看到这里打印了age
的值,且是通过setAge()
方法进行设置值。
注意:Spring是通过setter方法注入简单值的,而且这值的类型并不做区分,即你注入int类型值和注入String类型值是一样的,Spring会根据setter方法将你注入的值类型转换成指定的数据类型。
区分:Spring的<poperty>
和<constuctor-arg>
元素在很多地方是相似的。只不过前者是通过setter方法注入值,厚泽是通过构造器注入值的。
引用其他Bean
Spring的<constructor-arg>
可以注入对象引用,<property>
同样支持:
创建Child.java
对象
1 2 3 4 5 6 7 8 9 10 | package demo1; public class Child { private User user; public void setUser(User user) { this.user = user; } public User getUser() { return user; } } |
spring.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?xml version="1.0" encoding="utf-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="user" class="demo1.User"> <property name="age" value="18"/> </bean> <bean id="child" class="demo1.Child"> <property name="user" ref="user"/> </bean> </beans> |
Test测试类
1 2 3 4 5 6 7 | @Test public void run3(){ //加载Spring上下文 ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml"); Child child = (Child) ac.getBean("child"); System.out.println(child.getUser().getAge()); } |
打印结果:
注入内部Bean
改进上述代码:
spring.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?xml version="1.0" encoding="utf-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="user" class="demo1.User"> <property name="age" value="18"/> </bean> <bean id="child" class="demo1.Child"> <property name="user"> <bean class="demo1.User"/> </property> </bean> </beans> |
可以看到我们在<property>元素
内右嵌套了一个<bean>
,这中技术称为注入内部Bean,我们观察打印结果:
我们发现,这里却没有打印我们名字是user
的Bean中注入的数据,这就体现注入内部Bean的特点就是被注入的Bean对象中的数据不会被影响。这也体现了内部Bean的最大一个缺点就是:不能被复用。只适合一次注入,而且不能被其他Bean所引用(即我们给内部Bean配置id
属性是毫无意义的)。
3.3 使用Spring的p命名空间
Spring提供了一个命名空间p
用来作为<bean>
元素所有属性的前缀来装配Bean的属性,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 | <?xml version="1.0" encoding="utf-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- <bean id="user" class="demo1.User"> <property name="age" value="18"/> </bean>--> <bean id="user" class="demo1.User" p:age="19"/> </beans> |
装配集合
以上我们仅了解了Spring配置简单的属性值(使用value或ref属性),但value
和ref
都只是在配置单个值的情况下可以,那么对于集合类型,该怎么处理呢?
Spring提供了一些相应的集合配置元素:
- : 装配list类型的值,允许重复
- : 装配set类型的值,不允许重复
- : 装配map类型的值,名称和值可以是任意类型的
- : 装配properties类型的值,名称和值必须都是String类型
当装配类型是java.util.Collection
任意实现的属性时,<list>
和 <set>
几乎可以互用,所以装配的类型和选择的元素没有任何关系。<map>
和<props>
这两个元素分别对应java.util.Map
和java.util.Properties
。当我们需要由键-值组成的集合时,常用这两个元素。
举例:我们改变Child.java
的代码,如下:
1 2 3 4 5 6 7 8 9 10 11 | package demo1; import java.util.Map; public class Child { private Map<String, String> childMap; public void setChildMap(Map<String, String> childMap) { this.childMap = childMap; } public Map<String, String> getChildMap() { return childMap; } } |
spring.xml
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:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="child" class="demo1.Child"> <property name="childMap"> <map> <entry key="1" value="TyCoding"/> <entry key="2" value="涂陌"/> </map> </property> </bean> </beans> |
Test测试类
1 2 3 4 5 6 7 8 9 | @Test public void run3(){ //加载Spring上下文 ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml"); Child child = (Child) ac.getBean("child"); for (Object obj : child.getChildMap().values()){ System.out.println(obj); } } |
打印结果:
SpEL表达式
Spring的SpEL表达式具有以下特性:
- 使用Bean的ID来引用Bean;
- 调用方法和访问对象的属性;
- 对值进行算术、关系和逻辑运算;
- 集合操作;
语法:
1. 用 `#{}`标记的内容是SpEL表达式
2. 代替`ref`将一个bean装配到另一个bean中:`#{bean}`即可
3. 通过bean的引用来获取bean的属性:`#{bean.value}`
4. ...
总结之—Bean的生命周期
首先我们从源码开始分析:
以上是Spring Bean
从初始化到销毁所经历的方法,那么下面我们来画一个具体的流程图:
综上:再强调几点:
- Spring实例化一个Bean,通常就是我们所说的
new
操作。 - Spring上下文对实例化Bean进行配置,也就是IOC注入。
- 对于多例模式而言,在Spring对该Bean进行了初始化之后,就不会再对此Bean的后续生命周期管理,从上面的
destory-method
方法可以验证得到。 -
容器是Spring的核心,但是并不存在单一的容器。Spring自带几种容器实现,可以归纳为两种不同的类型。
- Bean工厂: 由
org.springframework.beans.factory.BeanFactory
接口定义,是最简单的容器。 - 应用上下文: 由
org.springframework.context.ApplicationContext
接口定义,基于BeanFactory
之上构建,并提供面向应用的服务。
- 以上流程并不是每个
Spring Bean
一定都会经历的,只有实现了对应的接口,才会实现对应的功能。
- Bean工厂: 由