Spring之旅
什么是Spring?
-
Spring是一个分层的轻量级的开源Java框架
-
Spring的核心是
IOC(Inverse of Control 控制反转)
和AOP(Aspect Oriented Programming 面向切面编程)
-
在实际开发中,通常服务端采用三层架构,分别为
表示层(web),业务层(service),持久层(dao)
,Spring对每一层都提供了技术支持,比如
表示层
提供了与Spring MVC等框架的整合;
业务层
可以管理事务、记录日志等;
持久层
提供了与Mybatis、Hibernate等框架的整合
Spring的体系结构
Spring框架采用的是分层架构,Spring框架的发布版本包括了20个不同的模块,每个模块会有3个JAR文件(二进制类库、源码的JAR文件以及JavaDoc的JAR文件),可以去自行下载。
这些模块依据其所属的功能可以划分为6类不同的功能。
让我们逐一浏览Spring的模块,看看它们是如何构建起Spring整体蓝图的。
容器是Spring框架最核心的部分,它管理着Spring应用中bean的创建、配置和管理。在该模块中,包括了Spring bean工厂,它为Spring提供了DI的功能。基于bean工厂,我们还会发现有多种Spring应用上下文的实现,每一种都提供了配置Spring的不同方式。除了bean工厂和应用上下文,该模块也提供了许多企业服务,例如Email、JNDI访问、EJB集成和调度。
IOC的基本概念
什么是IoC
IoC(Inverse of Control)
是Spring容器的内核,AOP、声明式事务等功能再次基础上开花结果。IoC这个概念比较晦涩难懂,它包括很多内涵,设计代码解耦、设计模式、代码优化等问题的考量,我们试图通过一个小例子来说明这个概念:
//演员周星驰
public class ZhouXingChi{
public void talk() {
System.out.println("曾经有一份。。。。");
}
}
//大话西游
public class AChineseOdyssey {
//至尊宝在水帘洞带上金箍
public void waterCurtainCave(){
//①演员直接侵入剧本
ZhouXingChi zxc = new ZhouXingChi();
zxc.talk();
}
}
星爷演过的一部电影《大话西游》,是他比较出名的一部。其中有一个场景,至尊宝为了救紫霞仙子必须带上金箍变成孙悟空才能去打败牛魔王。在①处,作为具体角色扮演者的周星驰直接侵入剧本,是剧本和演员耦合在一起,如下图:
耦合具有两面性(two-headed beast)
。一方面,紧密耦合的代码难以测试、难以复用、难以理解,并且典型地表现出“打地鼠”式的bug特性(修复一个bug,将会出现一个或者更多新的bug)。另一方面,一定程度的耦合又是必须的——完全没有耦合的代码什么也做不了。为了完成有实际意义的功能,不同的类必须以适当的方式进行交互。总而言之,耦合是必须的,但应当被小心谨慎地管理。
一个明智的编剧在剧情创作时应围绕故事的角色进行,而不应考虑角色的具体饰演者,这样才可能在剧本拍摄时自由地选择任何合适的演员,而非绑定在一人身上。通过以上分析,我们知道需要为剧本的主人公至尊宝定义一个接口,让扮演者实现该接口:
public interface ZhiZunBao {
public void talk();
}
package cn.wk.chapter00;
public class ZhouXingChi implements ZhiZunBao {
public void talk() {
System.out.println("曾经有一份。。。。");
}
}
public class AChineseOdyssey {
public void waterCurtainCave(){
//①引入至尊宝角色接口
ZhiZunBao zhiZunBao = new ZhouXingChi();
//②通过接口展开剧情
zhiZunBao.talk();
}
}
在①处引入了剧本的角色--至尊宝,剧本的情节通过角色展开,在拍摄时角色由演员饰演,如②处所示。因此,大话西游、至尊宝、周星驰的类图关系如下图所示:
从上图可以看出,AChineseOdyssey
类同时依赖于ZhiZunBao
接口和ZhouXingChi
类,并没有达到我们所期望的剧本仅依赖于角色的目的。但是角色最终必须通过具体的演员才能完成拍摄,如何让ZhouXingChi和剧本无关而又能完成ZhiZunBao的具体动作呢?当然是在影片拍摄时,导演将ZhouXingChi
安排在ZhiZunBao
的角色上,导演负责剧本、角色、饰演者三者的协调控制,如下图:
通过引入导演,使得剧本和具体饰演者解耦,对应到软件中,导演就像一台装配器,安排演员表演具体的角色。IOC(Inverse of Control,控制反转)
是Spirng容器的内核,AOP、声明式事务等功能在次基础上开花结果,通过上面的例子说明了IOC的概念,他包括两方面的内容:
- 控制。
-
反转。
那到底是什么东西的“控制”被“反转”了呢?对应前面的例子,“控制”是指选择ZhiZunBao角色扮演者的控制权;“反转”是指这种控制权从《大话西游》剧本中移除,转交到导演的手中。对于软件来说,即某一接口具体实现类的选择控制权从调用类中移除,转交给第三方决定,即由Spring容器借由Bean配置。
后来经过业界的讨论,使用DI(Dependency Injection,依赖注入)
的概念来代替IOC
,即让调用类对某一接口实现类的依赖关系有第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。“依赖注入"这个名词显然比“控制反转”直接明了、易于理解。
IoC的类型
从注入方法上看,IoC主要可以分为3种类型:①构造函数注入、②属性注入③接口注入。Spring支持构造函数注入和属性注入。下面我们继续使用上面的例子说明这3种注入方式的区别
①构造函数注入
public class AChineseOdyssey {
//①注入至尊宝的饰演者
private ZhiZunBao zhiZunBao;
public AChineseOdyssey(ZhiZunBao zhiZunBao) {
this.zhiZunBao = zhiZunBao;
}
public void waterCurtainCave(){
zhiZunBao.talk();
}
}
AChineseOdyssey
的构造函数不关系具体由谁来饰演至尊宝这个角色,只要在①处传入的饰演者按照剧本完成对应的表演即可,角色的具体饰演者由导演来安排,如下面的代码:
public class Director {
public void direct(){
//①指定角色的饰演者
ZhiZunBao zhiZunBao = new ZhouXingChi();
//②注入具体饰演者到剧本中
AChineseOdyssey aChineseOdyssey = new AChineseOdyssey(zhiZunBao);
aChineseOdyssey.waterCurtainCave();
}
}
在①处导演安排周星驰饰演至尊宝,并在②处将周星驰“注入”到《大话西游》中即可开始水帘洞里的剧情。
②属性注入
有时,导演会发现,虽然至尊宝是影片的第一主角,但不是每个场景都需要他。在这种情况下构造函数注入不妥,可以考用属性注入:
public class AChineseOdyssey {
private ZhiZunBao zhiZunBao;
//属性注入方法
public void setZhiZunBao(ZhiZunBao zhiZunBao) {
this.zhiZunBao = zhiZunBao;
}
public void waterCurtainCave(){
zhiZunBao.talk();
}
}
public class Director {
public void direct() {
AChineseOdyssey aChineseOdyssey = new AChineseOdyssey();
ZhiZunBao zhiZunBao = new ZhouXingChi();
//调用属性setter注入
aChineseOdyssey.setZhiZunBao(zhiZunBao);
aChineseOdyssey.waterCurtainCave();
}
}
和构造函数注入时不同,实例化AChineseOdyssey
剧本时,并未制定任何饰演者,而是在实例化后,需要至尊宝出场时,才调用set
方法注入饰演者。按照类似的方式,白晶晶,二当家也可以在适当的时候注入进来。这样,导演就可以根据所拍剧情的不同,按需注入相应的角色
③接口注入(了解即可)
Spring中的IOC
虽然AChineseOdyssey
和ZhouXingChi
实现了解耦,AChineseOdyssey
无需关注角色实现类的实例化工作,但这些工作在代码中依然存在,只是转移到了Director
类中而已。假设某一制片人想改变这一局面,在选择某个剧本后,希望通过“媒体”海选或代理机构来选择导演、演员、让他们各司其职,那么剧本、导演、演员度实现了解耦。
所谓的“媒体”海选或代理机构,在程序领域就是一个第三方的容器,它帮助完成类的初始化与装配工作,让开发者从这些底层实现类的实例化、依赖关系装配等工作中解脱出来、专注于更有意义的业务逻辑开发工作。这无疑是一件令人向往的事情。Spring就是这样的一个容器,它通过配置文件或注解描述类和类之间的依赖关系,自动完成类的初始化和依赖注入工作:
<?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-4.0.xsd">
<!--实现类实例化-->
<bean id="zhiZunBao" class="cn.wk.chapter0.ZhouXingChi"/>
<bean id="aChineseOdyssey" class="cn.wk.chapter0.spring.AChineseOdyssey">
<property name="zhiZunBao">
<ref bean="zhiZunBao"></ref><!--通过ref建立依赖关系-->
</property>
</bean>
</beans>
public class AChineseOdyssey {
private ZhiZunBao zhiZunBao;
public void setZhiZunBao(ZhiZunBao zhiZunBao) {
this.zhiZunBao = zhiZunBao;
}
public void waterCurtainCave(){
zhiZunBao.talk();
}
}
public interface ZhiZunBao {
public void talk();
}
public class ZhouXingChi implements ZhiZunBao {
public void talk() {
System.out.println("曾经有一份。。。。");
}
}
下面代码运行:
@Test
public void test() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("chapter0bean.xml");
AChineseOdyssey aChineseOdyssey = (AChineseOdyssey) applicationContext.getBean("aChineseOdyssey");
aChineseOdyssey.waterCurtainCave();
}
通过
new ClassPathXmlApplicationContext("chapter0bean.xml")
;方式即可启动容器。在容器启动时,Spring根据配置文件的描述信息,自动实例化Bean并完成依赖关系的装配,从容器中即可返回准备就绪的Bean实例,后续可直接使用之。Spring为什么会有这种“神奇”的力量,仅凭一个简单的配置文件,就能魔法般地实例化并装配好程序所用的Bean呢?这种“神奇”的力量归功于Java语言本身的类反射功能。(此片文章不做概述,请见另一篇文章)
Spring具有非常大的灵活性,它提供了三种主要的依赖注入(装配)机制:
基于XML文件的显式装配。
基于注解(Annotation)的隐式装配。
(常用)
在Java中进行显式配置。(很少使用,了解即可)
我们在下一节将会讲到Bean到底是什么还有这些依赖方式的具体实现。
Spring中的Bean
在看电影的时候,你曾经在电影结束后留在位置上继续观看片尾字幕吗?一部电影需要由这么多人齐心协力才能制作出来,这真是有点令人难以置信!除了主要的参与人员——演员、编剧、导演和制片人,还有那些幕后人员——音乐师、特效制作人员和艺术指导,更不用说道具师、录音师、服装师、化妆师、特技演员、广告师、第一助理摄影师、第二助理摄影师、布景师、灯光师和伙食管理员(或许是最重要的人员)了。
现在想象一下,如果这些人彼此之间没有任何交流,你最喜爱的电影会变成什么样子?让我这么说吧,他们都出现在摄影棚中,开始各做各的事情,彼此之间互不合作。如果导演保持沉默不喊“开机”,摄影师就不会开始拍摄。或许这并没什么大不了的,因为女主角还呆在她的保姆车里,而且因为没有雇佣灯光师,一切处于黑暗之中。或许你曾经看过类似这样的电影。但是大多数电影(总之,都还是很优秀的)都是由成千上万的人一起协作来完成的,他们有着共同的目标:制作一部广受欢迎的佳作。
在这方面,一个优秀的软件与之相比并没有太大区别。任何一个成功的应用都是由多个为了实现某一个业务目标而相互协作的组件构成的。这些组件必须彼此了解,并且相互协作来完成工作。例如,在一个在线购物系统中,订单管理组件需要和产品管理组件以及信用卡认证组件协作。这些组件或许还需要与数据访问组件协作,从数据库读取数据以及把数据写入数据库。
但是,正如我们在前面所看到的,创建应用对象之间关联关系的传统方法(通过构造器或者查找)通常会导致结构复杂的代码,这些代码很难被复用也很难进行单元测试。如果情况不严重的话,这些对象所做的事情只是超出了它应该做的范围;而最坏的情况则是,这些对象彼此之间高度耦合,难以复用和测试。
在Spring中,对象无需自己查找或创建与其所关联的其他对象。相反,容器负责把需要相互协作的对象引用赋予各个对象。例如,一个订单管理组件需要信用卡认证组件,但它不需要自己创建信用卡认证组件。订单管理组件只需要表明自己两手空空,容器就会主动赋予它一个信用卡认证组件。
创建应用对象之间协作关系的行为通常称为装配(wiring),这也是依赖注入(DI)的本质。我们将介绍使用Spring装配 bean的基础知识。因为DI是Spring的最基本要素,所以在开发基于Spring的应用时,你随时都在使用这些技术。
那么,什么是Bean?
如果把Spring看作一个大型工厂,则Spring容器中的Bean就是该工厂的产品。要想使用这个工厂生产和管理Bean,就需要在配置文件中告诉它需要哪些Bean,以及需要使用何种方式将这些Bean装配到一起(Bean的本质就是Java中的类,而Spring中的Bean其实就是对实体类的引用,来生产Java类对象,从而实现生产和管理Bean)。
Bean工厂
Spring通过一个配置文件描述Bean及Bean 直接的依赖关系,利用Java语言的反射功能实例化Bean并简历Bean之间的依赖关系。Spring的IoC容器在完成这些底层工作的基础上,还提供了Bean实例缓存、生命周期管理、Bean实例代理、事件发布、资源转载等高级服务。
Bean工厂(org.springframework.beans.factory.BeanFactory
)是Spring框架最核心的接口,它提供了高级IoC的配置机制。BeanFactory
使管理不同类型的Java对象成为可能,应用上下文(ApplicationContext
)建立在BeanFactory
基础之上,提供了更多面向应用的功能。我们一般称BeanFactory为IoC容器,而称ApplicationContext
为应用上下文。但有时为了方便,也将ApplicationContext
成为Spring容器。
Spring容器会负责控制程序之间的关系,而不是由程序代码直接控制,下面通过代码演示容器怎么创建:
①BeanFactory
(实际开发并不多用,了解即可)
//创建BeanFactory实例时,需要提供Spring所管理容器的详细配置信息,这些信息通常采用XML文件形式管理,其加载语法如下
BeanFactory beanFactory = new XmlBeanFactory(new FileSystemResource(("F:/applicationContext.xml")));//xml配置文件的位置
Spring容器就像一台构造精妙的机器,我们通过配置文件向机器传达控制信息,机器就能够按照设定的模式工作。如果将Spring容器比作一辆汽车,那么可以将BeanFactory看成汽车的发动机,而ApplicationContext则是一辆完整的汽车,它不但包括发动机,还包括离合器、变速器及底盘、车身、电器设备等其他组件。在内部,各个组件按部就班、有条不紊地完成汽车的各项功能。
②ApplicationContext
(常用
)
/**
* ApplicationContext是BeanFactory的子接口,是另一种常见的Spring容器。
* 其不仅包含了BeanFactory的所有功能,还添加了对国际化、资源访问、事件传播等方面的支持。
* 创建ApplicationContext接口实例,通常采用2种方法
*/
//1.通过ClassPathXmlApplicationContext创建,其会从类路径classPath中寻找指定的xml配置文件,找到并装载完成ApplicationContext的实例化工作
ApplicationContext applicationContext1 = new ClassPathXmlApplicationContext("applicationContext.xml");
//2.通过FileSystemXmlApplicationContext创建,其会从指定的文件系统路径(绝对路径)中寻找指定的xml配置文件,找到并装载完成ApplicationContext的实例化工作
ApplicationContext applicationContext2 = new FileSystemXmlApplicationContext("F:");
注意:在Java项目中,会通过ClassPathXmlApplicationContext来实例化ApplicationContext容器。而在Web项目中,ApplicationContext容器的实例化工作会交由Web服务器来完成。
③WebApplicationContext
(常用
)
像ApplicationContext
是BeanFactory的子接口一样,WebApplicationContext
是ApplicationContext
的子接口,它是专门为Web应用
准备的。由于Web应用
比一般的应用拥有更多的特性,因此WebApplicationContext
扩展了ApplicationContext
,它允许从相对于Web根目录的路径中装载配置文件完成初始化工作。从WebApplicationContext
中可以获得ServletContext
的引用,整个Web应用上下文对象将作为属性放置到ServletContext
中,以便Web应用环境可以访问Spring应用上下文。
在非Web应用的环境中,Bean只有singleton
和prototype
两种作用域(后面会讲到)
。WebApplicationContext
为Bean添加了三个新的作用域:request
、session
和global session
。
WebApplicationContext
的初始化和BeanFactory
、ApplicationContext
有所区别,因为WebApplicationContext
需要ServletContext
实例,也就是说,它必须在拥有Web容器的前提下才能完成启动工作。有过Web开发经验的读者都知道,可以在web.xml
中配置自启动的Servlet
或定义Web容器监听器(ServletContextListener
),借助两者中的任何一个,就可以完成启动Spring Web 应用上下文
的工作,所以需要在web.xml
加入以下代码:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<!--声明Servlet容器监听器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--监听到Servlet容器启动时装载指定的Spring配置文件-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<!--DispatcherServlet主要用作职责调度工作,本身主要用于控制流程-->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-web.xml</param-value>
</init-param>
<!--配置Spring MVC什么时候启动,参数必须为整数:
如果为0或者大于0,则Spring MVC随着容器启动而启动
如果小于0,则在第一次请求进来的时候启动
-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>*.form</url-pattern>
</servlet-mapping>
</web-app>
ContextLoaderListener
的作用就是启动Servlet容器
时,读取在contextConfigLocation
中参数定义的xml文件
,自动装配配置文件(此处为WEB-INF目录下的applicationContext.xml文件
)的信息,并产生WebApplicationContext
对象,然后将这个对象放置在ServletContext
的属性里,这样我们只要得到Servlet
就可以得到WebApplicationContext
对象,并利用这个对象访问spring容器管理的bean。
简单来说,就是上面这段配置为项目提供了Spring支持,初始化了Ioc容器。
Bean的配置
Spring容器提供2种格式的配置文件分别为Properties文件(基本不用)和XML文件(最常使用)
。
XML配置文件如下:
<?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="bean1" class="cn.wk.Bean1" />
</beans>
Bean的实例化
在面向对象的程序中,想要使用某个对象,就需要先实例化这个对象。同样,在Spring中,要想使用容器中的Bean,也需要实例化Bean。实例化Bean有3中方式,分别为构造器实例化(最为常见)
、静态工厂方式实例化、实例工厂方式实例化。
①构造器实例化
构造器实例化是指Spirng容器通过Bean对应的类中默认的构造函数来实例化Bean。
1.创建Bean类:
package chapter02.instance;
public class Bean1 {
}
2.创建Spring配置chapter02instancebean1.xml文件并配置Bean1的实体类:
<?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="bean1" class="chapter02.instance.Bean1"/>
</beans>
3.创建测试类测试:
@Test
public void test() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("chapter02instancebean1.xml");
Bean1 bean1 = (Bean1) applicationContext.getBean("bean1");
System.out.println(bean1);
}
运行结果:
②静态工厂方式实例化
静态工厂是实例化Bean的另一种方式。该方式要求自己创建一个静态工厂的方法来创建Bean的实例。
1.创建Bean2类:
package chapter02.instance;
public class Bean2 {
}
2.创建Java工厂类加入静态方法获取Bean2实例:
package chapter02.instance;
public class Bean2Factory {
public static Bean2 createBean(){
return new Bean2();
}
}
3.创建配置文件chapter02instancebean2.xml,并配置工厂类Bean:
<?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="bean2" class="chapter02.instance.Bean2Factory" factory-method="createBean"/>
</beans>
4.创建测试类:
@Test
public void test() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("chapter02instancebean2.xml");
Bean2 bean2 = (Bean2) applicationContext.getBean("bean2");
System.out.println(bean2);
}
运行结果:
③实例工厂方式实例化
实例工厂是采用直接创建Bean实例的方式,在配置文件中,通过factory-bean属性配置一个实例工厂,然后使用factory-method属性确定使用工厂中的哪个方法。
1.创建Bean3的类:
package chapter02.instance;
public class Bean3 {
}
2.创建Java工厂类,在类中使用非静态方法获取Bean3实例:
package chapter02.instance;
public class Bean3Factory {
public Bean3 createBean(){
return new Bean3();
}
}
3.创建配置文件chapter02instancebean3.xml,并配置工厂类Bean:
<?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="bean3Factory" class="chapter02.instance.Bean3Factory"/>
<bean id="bean3" factory-bean="bean3Factory" factory-method="createBean"/>
</beans>
4.创建测试类:
@Test
public void test() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("chapter02instancebean3.xml");
Bean3 bean3 = (Bean3) applicationContext.getBean("bean3");
System.out.println(bean3);
}
运行结果:
Bean的作用域
Spring4.3为我们提供了Bean的其中作用域,如下图:
上表7种作用域中,
singleton
和prototype
是最常见的两种作用域singleton
是Spring容器默认的作用域
,当Bean的作用域为singleton
时,Spring容器就只会存在一个共享的Bean实例。 singleton
作用域对于无会话状态的Bean(如Dao组件、Service组件)来说,是最常见的选择。在配置文件中可以这样配置(默认配置即为singleton
,所以可以不去配置):
<bean id="bean" class="cn.wk.Bean" scope="singleton"/>
对需要保持会话状态的Bean
(如Struts2的Action类)应使用prototype作用域
。在使用prototype作用域时,Spring容器会为每个对该Bean的请求都创建一个新的实例。在配置文件中可以这样配置:
<bean id="bean" class="cn.wk.Bean" scope="prototype"/>
Bean的生命周期
我们知道Web容器中的Servlet拥有明确的生命周期,Spring容器中的Bean也拥有相似的生命周期。了解Spring中Bean的生命周期的意义就在于,可以利用Bean在其存活期间的特定时刻完成一些相关操作。这种时刻可能有很多,但一般情况下,常会在Bean的postinitiation(初始化后)
和predestruction(销毁前)
执行一些相关操作。
Spring容器可以管理Bean部分作用域的生命周期。
对于singleton作用域:在此作用域下,Spring能够精确地指定该Bean何时被创建,何时初始化完成,以及何时被销毁。
对于prototype作用域:此时Spring只负责创建,当容器创建了Bean实例后,Bean的实例就交给客户端代码来管理,Spring容器将不再跟踪其生命周期。
上面全部流程不需要全部掌握,知道有这么个流程,掌握初始化和销毁方法即可,过程如下:
1.创建BeanLife类:
package chapter02.instance;
public class BeanLife {
//构造方法实例化
public BeanLife() {
System.out.println("BeanLife的构造方法,出现代表BeanLife被实例化");
}
//初始化方法
public void initMethod(){
System.out.println("BeanLife被初始化");
}
//销毁方法
public void destroyMethod(){
System.out.println("BeanLife被销毁");
}
}
2.创建chapter02instancebeanLife.xml配置文件
<?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">
<!--scope="singleton",这个值是默认,在默认情况下Spring能全程跟踪bean的生命周期-->
<bean id="beanLife" class="chapter02.instance.BeanLife" init-method="initMethod" destroy-method="destroyMethod"/>
</beans>
3.创建测试类并运行:
@Test
public void test() {
//ClassPathXmlApplicationContext提供了关闭容器的方法,这样bean的销毁方法才会被调用,其父类ApplicationContext没有
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("chapter02instancebeanLife.xml");
context.close();
}
运行结果:
注意
:初始化方法和销毁方法需在配置文件指定,并不是Java中的方法写init或destroy就是初始化方法和销毁方法了。
Bean的装配(依赖注入)方式
前面提到过Bean的实例化,实例化完成后还需要装配后才能发挥其作用。汽车(含有属性,商标,车型等等)是一个Bean,发动机(含有属性,牌子,转速等等)也是一个Bean。实例化将不同Bean的属性注入进去,这个Bean就成型了。汽车由许多个组件(Bean)组成,所以要把这些Bean组装到一起。(通过ref来引用,此处没有演示)组成Bean的装配有3种方式:
(1)基于XML文件的显式装配:
基于XML文件方式的依赖注入又分为构造方法注入和set方法注入2种方式:
-
构造方法注入
其实就相当于我们平常写的Dog dog = new Dog("旺财",5,List集合);
这里交由Spring来完成而已。我们通过代码了解一下:
创建Dog类:
package chapter02.constructor;
import java.util.List;
public class Dog {
private String name;
private int age;
private List<String> food;
public Dog(String name, int age, List<String> food) {
this.name = name;
this.age = age;
this.food = food;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
", food=" + food +
'}';
}
}
创建并配置xml文件:
<?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="dog" class="chapter02.constructor.Dog">
<!--index代表变量的顺序,由0开始,value代表变量的值-->
<constructor-arg index="0" value="旺财"/>
<constructor-arg index="1" value="5"/>
<constructor-arg index="2">
<!--若变量中含有集合,需要这么写-->
<list>
<value>饭</value>
<value>鱼</value>
<value>鸡肉</value>
</list>
</constructor-arg>
</bean>
</beans>
测试类:
@Test
public void test() {
String xmlPath = "chapter02constructor.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
Object dog = applicationContext.getBean("dog");
System.out.println(dog);
}
运行结果:
注意:
Bean需要含有有参构造器。
-
set方法注入
其实就相当于我们平常写的Dog dog = new Dog();dog.setName("大黄");dog.setAge(3);dog.setFood(List集合);
原因同上。代码如下:
创建Dog类:
package chapter02.set;
import java.util.List;
public class Dog {
private String name;
private int age;
private List<String> food;
public Dog(){
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public void setFood(List<String> food) {
this.food = food;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
", food=" + food +
'}';
}
}
创建并配置xml文件:
<?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="dog" class="chapter02.set.Dog">
<property name="name" value="大黄"/>
<property name="age" value="3"/>
<property name="food">
<list>
<value>饭</value>
<value>鱼</value>
<value>鸡肉</value>
</list>
</property>
</bean>
</beans>
测试类:
@Test
public void test() {
String xmlPath = "chapter02set.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
Object dog = applicationContext.getBean("dog");
System.out.println(dog);
}
运行结果:
注意:
需要含有无参构造器。
哦,对了,在XML配置文件中声明DI时,会有多种可选的配置方案和风格。具体到构造器注入,有2种基本的配置可供选择:①对于<constructor-arg>元素可以用Spring3.0引入的c-命名空间代替 ②对于 <property>元素可以用p-命名空间代替,两者的区别在很大程度就是是否冗长繁琐,我们只需要在约束里面声明一下。
基于XML文件的装配可能会导致XML文件过于臃肿,给后续的维护和升级带来一定的困难。所以Spring引入了基于注解(Annotation)的隐式装配方式。
(2)基于注解(Annotation)的隐式装配。(常用
)
其中,注解
@Component、@Controller、@Service、@Repository
都是代表一个组件的(Bean)意思,只是@Controller、@Service、@Repository
分别代表控制层,服务层,数据访问层而已,将它们区别开来了。进行注入的时候,用@Autowired
或@Resource
注解进行注入,当然它们有点区别,有时候@Autowired
需要配合@Qualifier
使用。
我们通过一个例子来看一下:
UserDao
接口:
public interface UserDao {
//存储用户信息
void saveUser();
}
在UserDao
接口实现类UserDaoImpl
中添加注解@Repository
并命名 :
import org.springframework.stereotype.Repository;
@Repository("userDao")
public class UserDaoImpl implements UserDao {
@Override
public void saveUser() {
System.out.println("与数据库访问并存储用户信息");
}
}
UserService
接口:
public interface UserService {
void saveUser();
}
在UserService接口实现类UserServiceImpl中添加注解@Service并命名 :
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service("userService")
public class UserServiceImpl implements UserService{
@Resource(name="userDao")
private UserDao userDao;
@Override
public void saveUser() {
System.out.println("Service层开始调用Dao层");
this.userDao.saveUser();
}
}
UserController
类:
import org.springframework.stereotype.Controller;
import javax.annotation.Resource;
@Controller
public class UserController {
@Resource(name = "userService")
private UserService userService;
public void saveUser(){
System.out.println("Controller层开始调用Service层");
userService.saveUser();
}
}
代码写完了,该去创建配置文件并测试了。
配置文件(注意添加新的context约束哦,因为我们会使用到它):
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--使用context命名空间,在配置文件中开启相应的注解器-->
<context:annotation-config/>
<!--分别定义3个Bean实例-->
<bean id="userDao" class="cn.wk.chapter02.annotation.UserDaoImpl"/>
<bean id="userService" class="cn.wk.chapter02.annotation.UserServiceImpl"/>
<bean id="userController" class="cn.wk.chapter02.annotation.UserController"/>
</beans>
但是我们发现这样一个一个写bean还是好麻烦啊,可不可以自动完成呢?当然可以,我们可以只在配置文件加入下面这句代码即可,它会帮我们自动扫描指定包下的所有添加了注解的Bean,自动帮我们写id(将类名第一个字母变成小写),class。将配置文件内容改一下,让其自动扫描:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="cn.wk.chapter02.annotation"/>
</beans>
最后就是测试类了:
@Test
public void test() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("chapter02annotation.xml");
UserController userController = (UserController) applicationContext.getBean("userController");
userController.saveUser();
}
运行结果如下图:
补充:前面提到过@Autowired
或@Resource
都可以进行注入操作,有什么区别?
@Autowired
默认按类型装配(这个注解是属于Spring包的),默认情况下必须要求依赖对象必须存在(且不能有多个依赖对象),如果要允许null
值,可以设置它的required
属性为false
,如:@Autowired(required=false)
;如果我们想使用名称装配可以结合@Qualifier
注解进行使用,如
@Autowired() @Qualifier("userDao")
private UserDao userDao;
@Resource
是JDK1.6支持的注解,默认按照名称进行装配,名称可以通过name
属性进行指定,如果没有指定name
属性,当注解写在字段上时,默认取字段名,按照名称查找,如果注解写在setter
方法上默认取属性名进行装配。当找不到与名称匹配的bean时才按照类型进行装配。但是需要注意的是,如果name
属性一旦指定,就只会按照名称进行装配。
至于,到底是用@Autowired
还是@Resource
,看你自己吧。。。
(3)自动化装配(可以忽略)
补充:FactoryBean接口
前面将到过BeanFactory
,BeanFactory
是个Factory
,也就是IOC容器或对象工厂,而FactoryBean
是个Bean。在Spring中,所有的Bean都是由BeanFactory
(也就是IOC容器)来进行管理的。但对FactoryBean
而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。
一般情况下,Spring通过反射机制利用<bean>的class
属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在<bean>中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean
的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。下面为该接口代码:
package org.springframework.beans.factory;
public interface FactoryBean<T> {
T getObject() throws Exception;
Class<?> getObjectType();
boolean isSingleton();
}
-
T getObject()
:返回由FactoryBean创建的Bean实例,如果isSingleton()
返回true
,则该实例会放到Spring容器中单实例缓存池中; -
booleanisSingleton()
:返回由FactoryBean
创建的Bean实例的作用域是singleton
还是prototype
; -
Class<T>getObjectType()
:返回FactoryBean
创建的Bean类型。
当配置文件中<bean>
的class
属性配置的实现类是FactoryBean
时,通过getBean()
方法返回的不是FactoryBean
本身,而是FactoryBean
接口实现类getObject()
方法所返回的对象,相当于FactoryBean的getObject()
方法代理了getBean()
方法。
在前面的例子中,在配置Dog类时,Dog的每个属性分别对于一个<property>
元素标签。假设我们认为这种方式不够简洁,而希望通过逗号分隔的方式一次性为Dog的属性指定配置值,那么可以通过编写一个实现了Factory
接口的类来来实现。
import cn.wk.chapter02.set.Dog;
import org.springframework.beans.factory.FactoryBean;
public class DogFactoryBean implements FactoryBean {
private String dogInfo;
//接受逗号分隔的属性设置信息
public String getDogInfo() {
return dogInfo;
}
//实例化dog Bean
@Override
public Object getObject() throws Exception {
Dog dog = new Dog();
String[] infos = dogInfo.split(",");
dog.setName(infos[0]);
dog.setAge(Integer.parseInt(infos[1]));
return dog;
}
//返回的类型
@Override
public Class<?> getObjectType() {
return Dog.class;
}
//标识通过该FactoryBean返回的Bean是singleton的
@Override
public boolean isSingleton() {
return false;
}
}
有了这个DogFactoryBean
后,就可以在配置文件使用以下自定义的配置方式配置Dog
Bean:
<bean id = "dog" class="cn.wk.chapter02.factorybean.DogFactoryBean" p:dogInfo="大黄,3"/>
当调用getBean("dog")
时,Spring通过反射机制发现DogFactoryBean
类,这时Spring容器就调用它实现的接口方法getObject()
返回工厂类创建的对象。如果用户希望获取DogFactoryBean
的实例,则需要在使用getBean(beanName)
方法时显示地在beanName
前加上&
前缀,即getBean("&car")
。
参考资料
《精通Spring 4.x 企业应用开发》