(一)初识 Spring


简介

  对 Spring 而言:

  • 它是一个分层的轻量级的开源 Java 框架,其目的是用于简化企业级应用程序开发
  • 核心是 IOC(Inverse of Control 控制反转)与 AOP(Aspect Oriented Programming 面向切面编程)
  • 在实际开发中,通常服务端采用三层架构,分别为表示层( web),业务层( service ),持久层( dao ),而 Spring 对每一层都提供了技术支持,比如:
    • 表示层提供了与 Spring MVC 等框架的整合
    • 业务层提供了事务管理、日志记录等功能;
    • 持久层提供了与 Mybatis、Hibernate 等 ORM 框架的整合

体系架构

  Spring 框架采用的是分层架构,其发布版本包括了 20 个不同的模块,每个模块会有 3 个 JAR 文件(二进制类库、源码的 JAR 文件以及 JavaDoc 的 JAR 文件)

 Spring框架由 20 个不同的模块组成

  这些模块依据其所属的功能可以划分为 6 类不同的功能。

 Spring 框架由 6 个定义良好的模块分类组成

核心概念——容器

  容器是 Spring 框架最核心的部分,其管理着 Spring 应用中 Bean 的创建、配置和管理
  该模块中包含了 Spring Bean 工厂,其为 Spring 提供 DI 的功能。
  基于 Bean 工厂,又有不同的 Spring 应用上下文的实现,每一种都提供了配置 Spring 的不同方式。

  若想深入 Spring 的核心,需先得了解下什么是 IOC 。

IOC

什么是 IoC?

  IoC(Inverse of Control) 这个概念比较晦涩难懂,因为它包括很多内涵:

  • 设计代码之间的解耦
  • 运用了许多设计模式
  • 注重代码的优化

  下面通过一个小例子来说明这个概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 演员周星驰
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 )。
  另一方面,一定程度的耦合又是必须的——完全没有耦合的代码什么也做不了。为了完成有实际意义的功能,不同的类必须以适当的方式进行交互。

  总而言之,耦合是必须的,但应当被小心谨慎地管理。

  因此,一个明智的编剧在剧情创作时应围绕故事的角色进行,而不应考虑角色的具体饰演者,这样才可能在剧本拍摄时自由地选择任何合适的演员,而非绑定在一人身上。
  通过以上分析,我们知道应该为剧本的主人公至尊宝定义一个接口(多态的运用)让扮演者实现该接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface ZhiZunBao {
public void talk();
}

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 的概念,它其实包括 2 方面的内容:

  • 控制
  • 反转   

  那到底是什么东西的“控制”被“反转”了呢?
  对应前面的例子:

  • “控制”是指选择ZhiZunBao角色扮演者的控制权;
  • “反转”是指这种控制权从《大话西游》剧本中移除,转交到导演的手中。

  对软件而言,即某一接口具体实现类的选择控制权从调用类中移除,转交给第三方决定
  对 Spring 而言 ,将由 Spring 容器借由 Bean 配置。
  经过业界大量的讨论,后来使用 DI(Dependency Injection,依赖注入)来代替IoC,即让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。
  “依赖注入”这个名词显然比“控制反转”直接明了、易于理解。

IoC 的类型

  从注入方法上看, IoC 主要可以分为 3 种类型(Spring 中仅支持构造函数注入和属性注入):

  • 构造函数注入
  • 属性注入
  • 接口注入

  下面我们继续使用上面的例子说明这 3 种注入方式的区别。

① 构造函数注入

1
2
3
4
5
6
7
8
9
10
11
12
public class AChineseOdyssey {
// ①.注入至尊宝的饰演者
private ZhiZunBao zhiZunBao;

public AChineseOdyssey(ZhiZunBao zhiZunBao) {
this.zhiZunBao = zhiZunBao;
}

public void waterCurtainCave(){
zhiZunBao.talk();
}
}

  AChineseOdyssey的构造函数不关心具体由谁来饰演至尊宝这个角色,只要在 ① 处传入的饰演者按照剧本完成对应的表演即可,角色的具体饰演者由导演来安排,如下面的代码:

1
2
3
4
5
6
7
8
9
10
public class Director {
public void direct(){
// ①.指定角色的饰演者
ZhiZunBao zhiZunBao = new ZhouXingChi();
// ②.注入具体饰演者到剧本中
AChineseOdyssey aChineseOdyssey = new AChineseOdyssey(zhiZunBao);

aChineseOdyssey.waterCurtainCave();
}
}

  在 ① 处,导演安排周星驰饰演至尊宝,并在 ② 处将周星驰“注入”到《大话西游》中,这样就可以开始水帘洞里的剧情啦!

② 属性注入

  有时,导演会发现,虽然至尊宝是影片的第一主角,但不是每个场景都需要他。
  在这种情况下构造函数注入不妥,可以考虑用属性注入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

  虽然AChineseOdysseyZhouXingChi实现了解耦,AChineseOdyssey无需关注角色实现类的实例化工作,但这些工作在代码中依然存在,只是转移到了Director类中而已。
  假设某一制片人想改变这一局面,在选择某个剧本后,希望通过“媒体”海选或代理机构来选择导演、演员,让他们各司其职,那么剧本、导演、演员度才真正地实现了解耦。

  所谓的“媒体”海选或代理机构,在程序领域就是一个第三方的容器,它帮助完成类的初始化与装配工作,让开发者从这些底层实现类的实例化、依赖关系装配等工作中解脱出来、专注于更有意义的业务逻辑开发工作。

  而 Spring 就是这样的一个容器,它通过配置文件或注解描述类和类之间的依赖关系,自动完成类的初始化和依赖注入工作

  示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?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>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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("曾经有一份。。。。");
}
}

  下面为测试类代码:

1
2
3
4
5
6
@Test 
public void test() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("chapter0bean.xml");
AChineseOdyssey aChineseOdyssey = (AChineseOdyssey) applicationContext.getBean("aChineseOdyssey");
aChineseOdyssey.waterCurtainCave();
}

  运行结果:

1
曾经有一份。。。。

  通过new ClassPathXmlApplicationContext("chapter0bean.xml");方式启动容器后,Spring 将根据配置文件的描述信息,自动实例化 Bean 并完成依赖关系的装配,从容器中即可返回准备就绪的 Bean 实例,后续即可直接使用。

  Spring 为什么会有这种“神奇”的力量,仅凭一个简单的配置文件,就能化腐朽为神奇般地实例化并装配好程序所用的 Bean 呢?
  这种“神奇”的力量其实归功于 Java 的类反射机制。

  Spring 的 IoC 容器在完成这些底层工作的基础上,还提供了Bean实例缓存、生命周期管理、Bean实例代理、事件发布、资源转载等高级服务。

Spring中 的 Bean

  你在看电影后有继续观看片尾字幕吗?一部电影需要由那么多人齐心协力才能制作出来,真令人难以置信!除了主要的参与人员——演员、编剧、导演和制片人,还有许多幕后人员——音乐师、特效制作人员和艺术指导,更不用说道具师、录音师、服装师、化妆师、特技演员、广告师、助理摄影师、布景师、灯光师和厨师了。

  想象一下,若这些人彼此之间没有任何交流,你最喜爱的电影会变成什么样子?这么说吧,他们将在摄影棚中各做各的事情,彼此之间互不合作。若导演保持沉默不喊“开机”,摄影师就不会开始拍摄。因为没有雇佣灯光师,一切就处于黑暗之中。

  或许有这种成本极低的电影。但是大多数优质电影都由成千上万的人一起协作完成,他们有着共同的目标:制作一部广受欢迎的佳作。

  在这方面,一个优秀的软件与之相比并没有太大区别。任何一个成功的应用都是由多个为了实现某一个业务目标而相互协作的组件构成的。这些组件必须彼此了解,并且相互协作来完成工作。例如,在一个在线购物系统中,订单管理组件需要和产品管理组件以及信用卡认证组件协作。这些组件或许还需要与数据访问组件协作,从数据库读取数据以及把数据写入数据库。

  但是,正如我们在前面所看到的,创建应用对象之间关联关系的传统方法(通过构造器或者查找)通常会导致结构复杂的代码,这些代码很难被复用也很难进行单元测试。若情况不严重的话,这些对象所做的事情只是超出了它应该做的范围;而最坏的情况则是,这些对象彼此之间高度耦合,难以复用和测试。

  在 Spring 中,对象无需自己查找或创建与其所关联的其他对象。相反,容器负责把需要相互协作的对象引用赋予各个对象。例如,一个订单管理组件需要信用卡认证组件,但它不需要自己创建信用卡认证组件。订单管理组件只需要表明自己两手空空,容器就会主动赋予它一个信用卡认证组件。

  创建应用对象之间协作关系的行为通常称为装配,这也是依赖注入(DI)的本质。下面将介绍使用 Spring 装配 bean的基础知识。因为DI是 Spring 的最基本要素,所以在开发基于 Spring 的应用时,开发者随时都在使用这些技术。

那么,什么是 Bean?

  简单来说,Bean 的本质就是 Java 中的类,而 Spring 中的 Bean 其实就是对实体类的引用,用来生产 Java 类对象,从而实现生产和管理 Bean

Bean 的配置

  Spring 通过一个xml配置文件来描述 Bean 与 Bean 之间的依赖关系,利用 Java 中的反射机制来实例化 Bean ,并且建立不同 Bean 之间的依赖关系

  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">
<bean id="bean1" class="cn.wk.Bean1" scope="singleton"/>
<bean id="bean2" class="cn.wk.Bean2">
<property name="bean" ref="bean1"/>
</bean>
</beans>

  其中,<beans>元素的常用属性如下:

  • id:Bean 的唯一标识符,Spring 容器对 Bean 的配置及管理都通过该属性完成;
  • class:该属性指定了 Bean 的具体实现类,必须使用类的全限定名;
  • scope:代表 Bean 实例的作用域,默认为 singleton,详见后文

  若把 Spring 看作一个大型工厂,则 Spring 容器中的 Bean 就是该工厂的产品。要想使用这个工厂生产和管理 Bean ,就需要在配置文件中告诉它需要哪些 Bean ,以及需要使用何种方式将这些 Bean 装配到一起。

  那么,什么是 Bean 工厂?

Bean 工厂

  Bean 工厂为一个接口,即org.springframework.beans.factory.BeanFactory,其作为 Spring 框架最核心的接口,提供了高级 IoC 的配置机制,继承图如下:

  BeanFactory使管理不同类型的 Java 对象成为可能,ApplicationContext继承自BeanFactory,提供了更多面向应用的功能。
  一般称 BeanFactory 为 IoC 容器,而称ApplicationContext为应用上下文。但有时为了方便,也将ApplicationContext称为 Spring 容器。

  Spring 容器会负责控制程序之间的关系,而不是由程序代码直接控制,下面通过代码演示容器怎么创建:

① BeanFactory(了解)

1
2
//创建BeanFactory实例时,需要提供Spring所管理容器的详细配置信息,这些信息通常采用XML文件形式管理,其加载语法如下
BeanFactory beanFactory = new XmlBeanFactory(new FileSystemResource(("F:/applicationContext.xml")));//xml配置文件的位置

  Spring 容器就像一台构造精妙的机器,我们通过配置文件向机器传达控制信息,机器就能够按照设定的模式工作。若将 Spring 容器比作一辆汽车,那么可以将BeanFactory看成汽车的发动机,而 ApplicationContext 则是一辆完整的汽车,它不但包括发动机,还包括离合器、变速器及底盘、车身、电器设备等其他组件。在内部,各个组件按部就班、有条不紊地完成汽车的各项功能。

② ApplicationContext(常用)

1
2
3
4
5
6
7
8
9
/**
* 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(常用)

  WebApplicationContext是专门为 Web 应用准备的。由于 Web 应用比一般的应用具有更多的特性,因此WebApplicationContext扩展了ApplicationContext,它允许从相对于 Web 根目录的路径中装载配置文件完成初始化工作。
  从WebApplicationContext中可以获得ServletContext的引用,整个 Web 应用上下文对象将作为属性放置到ServletContext中,以便 Web 应用环境可以访问Spring 应用上下文。

  在非 Web 应用的环境中,Bean 只有singletonprototype两种作用域。而WebApplicationContext为 Bean 添加了三个新的作用域:

  • request
  • session
  • global session

  WebApplicationContext的初始化和BeanFactoryApplicationContext有所区别,因为WebApplicationContext需要ServletContext实例。
  也就是说,它必须在拥有 Web 容器的前提下才能完成启动工作。
  有过 Web 开发经验的读者都知道,可以在web.xml中配置自启动的Servlet或定义 Web 容器监听器(ServletContextListener),借助两者中的任何一个,就可以完成启动 Spring Web 应用上下文的工作,所以需要在web.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
<!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>classpath: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>/</url-pattern>
</servlet-mapping>
</web-app>

  ContextLoaderListener的作用就是启动Servlet容器时,读取在contextConfigLocation中参数定义的xml文件,自动装配配置文件(此处为 resources 目录下的applicationContext.xml文件)的信息,并产生 WebApplicationContext 对象,然后将这个对象放置在 ServletContext 的属性里,这样我们只要得到 Servlet 就可以得到 WebApplicationContext 对象,并利用这个对象访问 Spring 容器管理的bean。
  简单来说,上述配置为项目提供了 Spring 支持,初始化了 Ioc 容器。

Bean 的实例化

  在面向对象的程序中,想要使用某个对象,就需要先实例化这个对象。
  同样的,在 Spring 中,若想使用容器中的 Bean,也需要实例化 Bean。
  Bean 的实例化有以下 3 种方式:

  • 构造器实例化(最常见)
  • 静态工厂方式实例化
  • 实例工厂方式实例化。

① 构造器实例化

  构造器实例化是指 Spirng 容器通过 Bean 对应的类中默认的构造函数来实例化Bean。

  1.创建 Bean 类:

1
2
3
4
package chapter02.instance;

public class Bean1 {
}

  2.创建 Spring 配置文件chapter02instancebean1.xml并配置 Bean1 的实体类:

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 id="bean1" class="chapter02.instance.Bean1"/>
</beans>

  3.创建测试类测试:

1
2
3
4
5
6
@Test 
public void test() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("chapter02instancebean1.xml");
Bean1 bean1 = (Bean1) applicationContext.getBean("bean1");
System.out.println(bean1);
}

  4.运行结果:

img

② 静态工厂方式实例化

  静态工厂是实例化 Bean 的另一种方式。该方式要求自己创建一个静态工厂的方法来创建 Bean 的实例。
  1.创建 Bean2 类:

1
2
3
4
package chapter02.instance;

public class Bean2 {
}

  2.创建 Java 工厂类加入静态方法获取 Bean2 实例:

1
2
3
4
5
6
7
package chapter02.instance;

public class Bean2Factory {
public static Bean2 createBean(){
return new Bean2();
}
}

  3.创建配置文件 chapter02instancebean2.xml,并配置工厂类 Bean:

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 id="bean2" class="chapter02.instance.Bean2Factory" factory-method="createBean"/>
</beans>

  4.创建测试类:

1
2
3
4
5
6
@Test 
public void test() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("chapter02instancebean2.xml");
Bean2 bean2 = (Bean2) applicationContext.getBean("bean2");
System.out.println(bean2);
}

  5.运行结果:

img

③ 实例工厂方式实例化

  实例工厂是采用直接创建 Bean 实例的方式,在配置文件中,通过 factory-bean 属性配置一个实例工厂,然后使用factory-method属性确定使用工厂中的哪个方法。
  1.创建Bean3的类:

1
2
3
4
package chapter02.instance;

public class Bean3 {
}

  2.创建 Java 工厂类,在类中使用非静态方法获取Bean3实例:

1
2
3
4
5
6
7
package chapter02.instance;

public class Bean3Factory {
public Bean3 createBean(){
return new Bean3();
}
}

  3.创建配置文件chapter02instancebean3.xml,并配置工厂类Bean

1
2
3
4
5
6
7
<?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.创建测试类:

1
2
3
4
5
6
@Test 
public void test() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("chapter02instancebean3.xml");
Bean3 bean3 = (Bean3) applicationContext.getBean("bean3");
System.out.println(bean3);
}

  5.运行结果:

img

Bean 的作用域

  Spring 4.3 为我们提供了 7 种 Bean 的作用域,如下图:

作用域 说明
singleton(单例) 默认作用域,该方式定义的 Bean 在 Spring 容器中将只有一个实例。
换而言之,无论多少个 Bean 引用它,始终都指向同一个对象
prototype(原型) 每次通过 Spring 容器获取的该方式定义的 Bean 时,容器都将创建一个新的 Bean 实例
request 在一次 HTTP 请求中,容器会返回该 Bean 的一个实例。
对不同的 HTTP 请求则会产生一个新的 Bean,且该 Bean 仅在当前 HTTP Request 中有效
session 在一次 HTTP Session 中,容器会返回该 Bean 的一个实例。
对不同的 HTTP 请求则会产生一个新的 Bean,且该 Bean 仅在当前 HTTP Session 中有效
globalSession 在一个全局的 HTTP Session 中,容器会返回该 Bean 的一个实例。
该 Bean 仅在使用 portly 上下文时有效
application 为每个 ServletContext 对象创建一个实例。仅在 Web 相关的 ApplicationContext 中生效
websocket 为每个 websocket 对象创建一个实例。仅在 Web 相关的 ApplicationContext 中生效

  上表 7 种作用域中,singletonprototype是最常见的两种作用域

  singleton是 Spring 容器默认的作用域,当 Bean 的作用域为singleton时,Spring 容器就只会存在一个共享的 Bean 实例。
  singleton作用域对于无会话状态的Bean(如 Dao 组件、Service 组件)来说,是最常见的选择。
  在配置文件中可以这样配置(默认配置即为singleton,所以可以不去配置):

1
<bean id="bean" class="cn.wk.Bean" scope="singleton"/>

  对需要保持会话状态的 Bean (如 Struts2 的 Action 类)应使用prototype作用域。
  在使用prototype作用域时,Spring 容器会为每个对该 Bean 的请求都创建一个新的实例。在配置文件中可以这样配置:

1
<bean id="bean" class="cn.wk.Bean" scope="prototype"/>

Bean 的生命周期

  在 Web 容器中的Servlet拥有明确的生命周期,Spring 容器中的 Bean 也拥有相似的生命周期。
  为什么要了解 Bean 的生命周期呢?
  意义在于:可以利用 Bean 在其存活期间的特定时刻完成一些相关操作。
  这种时刻可能有很多,但一般情况下,常会在 Bean 的postinitiation(初始化后)和predestruction(销毁前)执行一些相关操作。

  Spring 容器可以管理 Bean 部分作用域的生命周期:

  • singleton:在此作用域下,Spring 能够精确地指定该 Bean 何时被创建,何时初始化完成,以及何时被销毁。
  • prototype:此作用域下 Spring 只负责创建,当容器创建了 Bean 实例后,Bean 的实例就交给客户端代码来管理,Spring 容器将不再跟踪其生命周期。

Bean的生命周期流程

① Spring 启动,查找并加载需要被 Spring 管理的 Bean,进行 Bean 的实例化

② 实例化完成后通过 IoC 方式注入引用和值到 Bean 的属性中

③ 若 Bean 实现了 BeanNameAware 接口,Spring 将 Bean 的 id 传递给 setBeanName() 方法

④ 若 Bean 实现了 BeanFactoryAware 接口,Spring 将调用 setBeanFactory() 方法,将 BeanFactory 容器实例传入

⑤ 若 Bean 实现了 ApplicationContextAware 接口, Spring 将调用 Bean 的 setApplicationContext() 方法,将 Bean 所在应用上下文引用传入进来。

⑥ 若 Bean 实现了 BeanPostProcessor 接口, Spring 将调用该接口的预初始化方法,即 postProcessBeforeInitialization() 方法。

⑦ 若 Bean 实现了 InitializingBean 接口,Spring 将调用他们的 afterPropertiesSet() 方法。类似的,若 bean 使用 init-method 声明了初始化方法,该方法也会被调用

⑧ 若 Bean 实现了 BeanPostProcessor 接口, Spring 将调用该接口的初始化方法,即 postProcessAfterInitialization() 方法。

⑨ 此时 Bean 已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。

⑩ 若 Bean 实现了 DisposableBean 接口,Spring 将调用它的 destory() 接口方法,同样,若 bean 使用了 destory-method 声明销毁方法,该方法也会被调用。

Bean 的装配(依赖注入)方式

  前面提到过 Bean 的实例化,在实例化完成后还需要装配后才能发挥其作用。
  举个例子,比如汽车(含有属性,商标,车型等等)是一个Bean,发动机(含有属性,牌子,转速等等)也是一个 Bean。实例化时会将不同 Bean 的属性注入进去,这个 Bean 就成型了。汽车由许多个组件( Bean )组成,所以要把这些 Bean 组装到一起。(通过 ref 来引用)。

  Spring 灵活性地提供了三种依赖注入(装配)机制:

  • 基于 xml 文件的显式装配
  • 基于注解(Annotation)的隐式装配(常用)
  • 在 Java 中进行显式配置

(1) 基于 XML 文件的显式装配:

  基于 XML 文件方式的依赖注入又可以分为:

  • 构造方法注入
  • set方法注入

构造方法注入

  其实就相当于在 Java 中编写的下面的代码:

1
Dog dog = new Dog("旺财",5,List集合);

  只是这里交由 Spring 来完成而已。下面通过代码了解一下:

  创建Dog类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 文件:

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">
<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>

  测试类:

1
2
3
4
5
6
7
@Test 
public void test() {
String xmlPath = "chapter02constructor.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
Object dog = applicationContext.getBean("dog");
System.out.println(dog);
}

  运行结果:

注意哦:Bean 需要含有有参构造器。

set 方法注入

  其实相当于下面的代码:

1
2
3
4
Dog dog = new Dog();
dog.setName("大黄");
dog.setAge(3);
dog.setFood(List集合);

  原因同上,代码如下:

  创建 Dog 类:

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
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 文件:

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"
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>

  测试类:

1
2
3
4
5
6
7
@Test 
public void test() {
String xmlPath = "chapter02set.xml";
ApplicationContext applicationContext = new ClassPathXmlApplicationContext(xmlPath);
Object dog = applicationContext.getBean("dog");
System.out.println(dog);
}

  运行结果:

img

注意哦:Bean 需要含有无参构造器。

(2)基于注解(Annotation)的隐式装配(常用

  因为基于 XML 文件的装配导致 XML 文件过于臃肿,使后续维护和升级变得非常困难,所以 Spring 引入了基于注解(Annotation)的隐式装配方式。
  下面是一些主要的注解:

注解 说明
@Component 用于描述 Spring 中的 Bean,仅仅表示一个组件
@Repository 用于将数据访问层(DAO)的类标识为 Spring 中的 Bean
@Service 用于将业务层(Service)的类标识为 Spring 中的 Bean
@Controller 用于将控制层(Controller)的类标识为 Spring 中的 Bean
@Autowired 用于对 Bean 的属性变量、属性的 setter 方法及构造方法进行标注,
配合对应的注解处理器完成 Bean 的自动配置工作
@Qualifier 与 @Autowired 配合使用,会将默认的按 Bean 类型装配修改为按 Bean 的实例名称装配,
Bean 的实例名称由 @Qualifier 的参数指定
@Resource 作用与 @Autowired 一样,但其包含 name、type 属性。
Spring 将 name 属性解析为 Bean 实例名称,type 属性解析为 Bean 实例类型

  其中,注解@Component、@Controller、@Service、@Repository都是代表一个组件(Bean)的意思,只是@Controller、@Service、@Repository分别代表控制层,服务层,数据访问层而已,这么做易于区分。
  在进行注入的时候,需要使用@Autowired@Resource注解,当然它们有点区别,有时候@Autowired需要配合@Qualifier使用。

  下面通过一个例子来看一下:
  UserDao接口:

1
2
3
4
public interface UserDao {
//存储用户信息
void saveUser();
}

  在UserDao接口实现类UserDaoImpl中添加注解@Repository并命名 :

1
2
3
4
5
6
7
8
9
import org.springframework.stereotype.Repository;

@Repository("userDao")
public class UserDaoImpl implements UserDao {
@Override
public void saveUser() {
System.out.println("与数据库访问并存储用户信息");
}
}

  UserService接口:

1
2
3
public interface UserService {
void saveUser();
}

  在UserService接口实现类UserServiceImpl中添加注解@Service并命名 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 约束哦,因为我们会使用到它):

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: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 还是好麻烦啊,可不可以自动完成呢?

  当然是可以的,只需在配置文件加入下面这句代码即可:

1
<context:component-scan base-package="cn.wk.chapter02.annotation"/>

  它会帮我们自动扫描指定包下的所有添加了注解的 Bean,自动帮我们写id(将类名第一个字母变成小写)及 class。

  最后就是测试类了:

1
2
3
4
5
6
@Test 
public void test() {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("chapter02annotation.xml");
UserController userController = (UserController) applicationContext.getBean("userController");
userController.saveUser();
}

  运行结果如下图:

img

补充:@Autowired 和 @Resource 的区别

  这两个注解都可以进行注入操作,那么有什么区别呢?

@Autowired

  @Autowired是属于 Spring 包的,它可以对类成员变量、方法及构造函数进行标注,默认根据类型(byType)完成自动装配的工作。
  比如下面的代码:

1
2
@Autowired
public IUserService userService;

  系统将根据IUserService接口进行注入,分以下 3 种情况:

  • 若该接口只有一个实现类,那么会正常注入
  • 若该接口有多个实现类,则报错
  • 若没有实现类,也会报错

  对上面的错误问题,可以进行限定,如下面的代码:

1
2
@Autowired(required = false)   
public IUserService userService

  若找不到对应的 bean,则不会抛出错误若required = true,当不存在 bean 时,将抛出异常

  若想根据名称自动装配,可以用@Qualifier来配合@Autowird一起使用。
  @Autowired@Qualifier结合使用时,自动注入的策略将从byType转变成byName,比如下面的代码:

1
2
3
4
5
6
7
8
@Autowired
@Qualifier("userServiceImpl")
public IUserService userService;

@Autowired
public void setUserDao(@Qualifier("userDao") UserDao userDao) {
this.userDao = userDao;
}

  此做法 Spring 将根据@Qualifier指定的 id 进行注入(依据 id 查询对应到 bean)

@Resource

  @Resource 是 JDK 1.6 支持的注解,和@Autowired作用一样,只不过@AutowiredbyType自动注入,而@Resource默认按byName自动注入。

  @Resource以下两个属性比较重要:

  • name:Spring 将@Resource注解的name属性解析为bean的名字(id
  • type:Spring 将@Resource注解的type属性解析为bean的类型

  对于以上属性的使用,分为 4 种情况:

  • 若使用name属性,则使用byName自动注入策略
  • 若使用type属性,则使用byType自动注入策略
  • 若同时使用,由于name属性优先级高,因此只会按照名称进行装配
  • 若既不指定name也不指定type,此时将通过反射机制使用byName自动注入策略

  @Resource具体的装配顺序如下:

  • 若同时指定了nametype,则从 Spring 上下文中找到唯一匹配的 bean 进行装配,找不到则抛出异常
  • 若指定了name,则从 Spring 上下文中查找与名称(id)匹配的 bean 进行装配,找不到则抛出异常
  • 若指定了type,则从 Spring 上下文中找到类型匹配的唯一 bean 进行装配,找不到或者找到多个,都会抛出异常
  • 若既没有指定name,又没有指定type,则自动按照byName方式进行装配;
  • 若没有匹配,则回退为一个原始类型进行匹配

(3) 在 Java 中进行显式配置(Spring Boot 中常用哦)

补充:FactoryBean 接口

  前面将到过BeanFactoryBeanFactory是个Factory,也就是 IOC 容器或对象工厂,而FactoryBean是个 Bean。在 Spring 中,所有的 Bean 都是由BeanFactory来进行管理的。
  但对FactoryBean而言,这个 Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂 Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。
  一般情况下,Spring 通过反射机制利用<bean>class属性指定实现类实例化 Bean,在某些情况下,实例化 Bean 过程比较复杂,若按照传统的方式,则需要在<bean>中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。
  为此,Spring提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化 Bean 的逻辑。
  下面为该接口代码:

1
2
3
4
5
6
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接口的类来来实现。

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
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后,就可以在配置文件使用以下自定义的配置方式配置DogBean:

1
<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 企业应用开发 [M]. 电子工业出版社,2017

文章信息

时间 说明
2019-03-06 初稿
0%