Spring之旅
Spring是一个轻量级的开源Java框架
Spring的优势就是分层架构
JavaEE开发分为三层结构:
Web层 -->SpringMVC
业务层 -->Bean管理(IOC)
持久层 -->Spring的JDBC模板、ORM模板用于整合其他持久层框架
Spring通过装配Bean对象来完成各个应用之间的协同合作,这也是依赖注入的本质。而依赖注入即是我们前文提到的IOC
控制反转的思想-->通过将应用对象装配进Spring Bean
中,即由Spring管理对象的依赖关系。
Spring是一个基于容器的框架,我们需要通过配置告诉Spring去加载哪些Bean和如果装配这些Bean。
配置Spring的方式有两种:
在XML文件中声明Bean
通过注解配置Spring
首先以下是一个基本的Spring XML配置:
<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>
在
元素内,可以配置所有的Spring的配置信息。而beans
不是唯一的Spring命名空间,Spring常用的命名空间有: * aop: 为声明切面以及将@AspectJ注解的类代理为Spring切面提供了配置元素 * beans: 支持声明bean和装配bean,是Spring最基本的命名空间 * context: 为配置Spring应用上下文气功配置元素,包括自动检测和自动装配Bean、注入非Spring直接管理的对象 * jms: 为声明小气驱动的POJO提供配置元素 * mvc: 启用SpringMVC,例如面向注解的控制器、视图解析器和拦截器 * tx:提供声明式事务配置
创建一个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管理
<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的呢?
改进上述代码
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
<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"/>
<bean id="user2" class="demo1.User">
<constructor-arg value="12"/>
bean>
beans>
如上所示:在
中使用
元素来告诉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
<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"/>
<bean id="user2" class="demo1.User">
<constructor-arg value="12"/>
bean>
beans>
打印结果:
综上
我们发现Spring会通过JavaBean的默认无参构造来实例化对象,并提供一种特殊的构造方法:构造器
来实例化Bean对象(但其实是使用了Bean对象的带参构造函数),构造器
的特点就是按需实例化
Spring支持实例化对象时提供额外的信息来覆盖本身定义的数据。
spring.xml
<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"/>
<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
添加:
public User(String name, User user){ System.out.println("打印name的值和age的值:" + name + "," + user.getAge()); }
打印结果:
同上上面的构造器注入方式我们发现,使用构造器注入,其实是使用JavaBean中的构造方法,而我们在构造器
中注入什么值,那么在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
中添加如下:
<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。这样正印证了
的特点:只有在调用
指定的方法时才开始实例化Bean,而不是加载spring.xml
时就实例化。
所有的Spring默认都是单例,当容器实例化一个Bean时,无论是通过装配(
),还是通过getBean()
(调用默认的无参构造),都会返回Bean的同一个实例。那么如何覆盖Spring的默认单例配置呢?
通过将scope="property"
即可,如下:
<bean id="user" class="demo1.User" scope="prototype"/>
首先我们仍用上面的例子,但让其实例化两次Bean对象,如下:
spring.xml
<bean id="user" class="demo1.User"/>
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
<bean id="user" class="demo1.User" scope="prototype"/>
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上下文中有效。
Spring提供了Bean声明周期的钩子方法,用来在Bean初始化和销毁前执行。
初始化:init-method
—>初始化后调用
销毁: destory-method
—>销毁前调用
举例:
spring.xml
<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" init-method="turnOn" destroy-method="turnOff"/>
beans>
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测试类
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
<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
对象的操作。
如果Spring的上下文中出现较多的Bean需要同一个初始化、销毁方法。那么我们可以定义一个全局的方法:
在
中定义:
default-init-method="turnOn"
default-destory-method="turnOff"
通常JavaBean的属性是私有的,同时拥有一组存取器方法,以setXxx()
和getXxx()
形式存在,而Spring就可以借助setXxx()
方法里配置属性的值,以实现setter
方法注入,请看下面。
spring.xml
<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>
beans>
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测试方法
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方法注入值,厚泽是通过构造器注入值的。
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
<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测试类
public void run3(){ //加载Spring上下文 ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml"); Child child = (Child) ac.getBean("child"); System.out.println(child.getUser().getAge()); }
打印结果:
改进上述代码:
spring.xml
<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>
可以看到我们在
内右嵌套了一个
,这中技术称为注入内部Bean,我们观察打印结果:
我们发现,这里却没有打印我们名字是user
的Bean中注入的数据,这就体现注入内部Bean的特点就是被注入的Bean对象中的数据不会被影响。这也体现了内部Bean的最大一个缺点就是:不能被复用。只适合一次注入,而且不能被其他Bean所引用(即我们给内部Bean配置id
属性是毫无意义的)。
Spring提供了一个命名空间p
用来作为
元素所有属性的前缀来装配Bean的属性,如下所示:
<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" p:age="19"/>
beans>
以上我们仅了解了Spring配置简单的属性值(使用value或ref属性),但value
和ref
都只是在配置单个值的情况下可以,那么对于集合类型,该怎么处理呢?
Spring提供了一些相应的集合配置元素:
: 装配list类型的值,允许重复
: 装配map类型的值,名称和值可以是任意类型的
当装配类型是java.util.Collection
任意实现的属性时,
和
几乎可以互用,所以装配的类型和选择的元素没有任何关系。和
这两个元素分别对应java.util.Map
和java.util.Properties
。当我们需要由键-值组成的集合时,常用这两个元素。
举例:我们改变Child.java
的代码,如下:
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
<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测试类
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); } }
打印结果:
Spring的SpEL表达式具有以下特性:
使用Bean的ID来引用Bean;
调用方法和访问对象的属性;
对值进行算术、关系和逻辑运算;
集合操作;
语法:
用 #{}
标记的内容是SpEL表达式
代替ref
将一个bean装配到另一个bean中:#{bean}
即可
通过bean的引用来获取bean的属性:#{bean.value}
...
首先我们从源码开始分析:
以上是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
一定都会经历的,只有实现了对应的接口,才会实现对应的功能。
如果大家有兴趣,欢迎大家加入我的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