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配置:
在
元素内,可以配置所有的Spring的配置信息。而beans
不是唯一的Spring命名空间,Spring常用的命名空间有:
* aop: 为声明切面以及将@AspectJ注解的类代理为Spring切面提供了配置元素
* beans: 支持声明bean和装配bean,是Spring最基本的命名空间
* context: 为配置Spring应用上下文气功配置元素,包括自动检测和自动装配Bean、注入非Spring直接管理的对象
* jms: 为声明小气驱动的POJO提供配置元素
* mvc: 启用SpringMVC,例如面向注解的控制器、视图解析器和拦截器
* tx:提供声明式事务配置
声明一个简单的Bean
- 创建一个
User.java
接口
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管理
结果:
-
解释:
分析以上案例:我们首先创建一个JavaBean对象
User.java
,然后将这个JavaBean对象交给Spring管理—>即在Spring配置文件中注入。那么,Spring是怎么实例化这个名字叫user
的Bean的呢? 改进上述代码
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
如上所示:在
中使用
元素来告诉Spring额外的信息,但是我们直接这样写是会报错的:
通过IDEA的报错提示我们发现解决办法是要在User.java
中创建一个带参构造函数,所以我们创建如下带参构造函数:
为了更直观的展示,我们改进上述代码:
User.java
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
打印结果:
综上
我们发现Spring会通过JavaBean的默认无参构造来实例化对象,并提供一种特殊的构造方法:构造器
来实例化Bean对象(但其实是使用了Bean对象的带参构造函数),构造器
的特点就是按需实例化
Spring支持实例化对象时提供额外的信息来覆盖本身定义的数据。
构造器另一种用法—>注入对象引用
spring.xml
在User.java
添加:
public User(String name, User user){
System.out.println("打印name的值和age的值:" + name + "," + user.getAge());
}
打印结果:
总结
同上上面的构造器注入方式我们发现,使用构造器注入,其实是使用JavaBean中的构造方法,而我们在构造器
中注入什么值,那么在Bean对象带参构造函数中的参数列表中就应该用什么值接收,比如我们,传入的是一个引用对象,那么就应该用对象最后接收参数。
通过工厂方法创建Bean
一般来说,单例类的实例只能通过静态工厂方法来创建。Spring支持通过
元素的factory-method
属性来装配工厂创建的Bean。
以下是一个典型的单例类:
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
中添加如下:
注:这样应用的场景是我们不想在加载spring.xml
时就实例化Bean,而是调用factory-method
所指定的方法时,才开始真正的实例化Bean。
使用要静态工厂创建Bean要注意:这里的class
属性并不是指定Bean实例的实现类,而是静态工厂类。因为Spring需要知道是用哪个工厂承诺噶来创建Bean的实例。其次factory-method
指定的是静态工厂方法名(必须是静态的)。
如下:
运行结果(注意:此时我们测试的是run2()
方法):
发现:此时我们没有调用run1()
方法,但是两次调用run2()
方法都已经实例化了名字是user
的Bean,但是,没有调用getBean()
实例化名字是case
的Bean,Spring就不会实例化该Bean。这样正印证了
的特点:只有在调用
指定的方法时才开始实例化Bean,而不是加载spring.xml
时就实例化。
Bean的作用域
所有的Spring默认都是单例,当容器实例化一个Bean时,无论是通过装配(
),还是通过getBean()
(调用默认的无参构造),都会返回Bean的同一个实例。那么如何覆盖Spring的默认单例配置呢?
通过将scope="property"
即可,如下:
首先我们仍用上面的例子,但让其实例化两次Bean对象,如下:
spring.xml
Test测试类
@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
Test测试类
@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
User.java
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测试类
@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
然后我们观察运行结果:
源码分析
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需要同一个初始化、销毁方法。那么我们可以定义一个全局的方法:
在
中定义:
default-init-method="turnOn"
default-destory-method="turnOff"
注入Bean属性
通常JavaBean的属性是私有的,同时拥有一组存取器方法,以setXxx()
和getXxx()
形式存在,而Spring就可以借助setXxx()
方法里配置属性的值,以实现setter
方法注入,请看下面。
spring.xml
User.java
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测试方法
@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的
和
元素在很多地方是相似的。只不过前者是通过setter方法注入值,厚泽是通过构造器注入值的。
引用其他Bean
Spring的
可以注入对象引用,
同样支持:
创建Child.java
对象
package demo1;
public class Child {
private User user;
public void setUser(User user) {
this.user = user;
}
public User getUser() {
return user;
}
}
spring.xml
Test测试类
@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
可以看到我们在
内右嵌套了一个
,这中技术称为注入内部Bean,我们观察打印结果:
我们发现,这里却没有打印我们名字是user
的Bean中注入的数据,这就体现注入内部Bean的特点就是被注入的Bean对象中的数据不会被影响。这也体现了内部Bean的最大一个缺点就是:不能被复用。只适合一次注入,而且不能被其他Bean所引用(即我们给内部Bean配置id
属性是毫无意义的)。
3.3 使用Spring的p命名空间
Spring提供了一个命名空间p
用来作为
元素所有属性的前缀来装配Bean的属性,如下所示:
装配集合
以上我们仅了解了Spring配置简单的属性值(使用value或ref属性),但value
和ref
都只是在配置单个值的情况下可以,那么对于集合类型,该怎么处理呢?
Spring提供了一些相应的集合配置元素:
- : 装配list类型的值,允许重复
: 装配set类型的值,不允许重复 : 装配properties类型的值,名称和值必须都是String类型
当装配类型是java.util.Collection
任意实现的属性时,
和
几乎可以互用,所以装配的类型和选择的元素没有任何关系。和
这两个元素分别对应java.util.Map
和java.util.Properties
。当我们需要由键-值组成的集合时,常用这两个元素。
举例:我们改变Child.java
的代码,如下:
package demo1;
import java.util.Map;
public class Child {
private Map childMap;
public void setChildMap(Map childMap) {
this.childMap = childMap;
}
public Map getChildMap() {
return childMap;
}
}
spring.xml
Test测试类
@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
之上构建,并提供面向应用的服务。
- Bean工厂: 由
以上流程并不是每个
Spring Bean
一定都会经历的,只有实现了对应的接口,才会实现对应的功能。
交流
如果大家有兴趣,欢迎大家加入我的Java交流群:671017003 ,一起交流学习Java技术。博主目前一直在自学JAVA中,技术有限,如果可以,会尽力给大家提供一些帮助,或是一些学习方法,当然群里的大佬都会积极给新手答疑的。所以,别犹豫,快来加入我们吧!
联系
If you have some questions after you see this article, you can contact me or you can find some info by clicking these links.
- Blog@TyCoding's blog
- GitHub@TyCoding
- ZhiHu@TyCoding