购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.2 Spring控制反转

1.2.1 IoC和DI

IoC(Inversion of Control,控制反转)是Spring最核心的要点,并且贯穿始终。IoC并不是一门技术,而是一种设计思想。在Spring框架中,实现控制反转的是Spring IoC容器,由容器来控制对象的生命周期和业务对象之间的依赖关系,而不是像传统方式(new对象)那样由代码来直接控制。程序中所有的对象都会在Spring IoC容器中登记,告诉容器该对象是什么类型、需要依赖什么,然后IoC容器会在系统运行到适当的时候把它要的对象主动创建好,同时也会把该对象交给其他需要它的对象。也就是说,控制对象生存周期的不再是引用它的对象,而是由Spring IoC容器来控制所有对象的创建、销毁。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被Spring IoC容器所控制,所以这叫控制反转。

控制反转最直观的表达就是,IoC容器让对象的创建不用去新建(new)了,而是由Spring自动生产,使用Java的反射机制,根据配置文件在运行时动态地去创建对象以及管理对象,并调用对象的方法。控制反转的本质是控制权由应用代码转到了外部容器(IoC容器)。控制权的转移就是反转。控制权的转移带来的好处是降低了业务对象之间的依赖程度,即实现了解耦。

DI(Dependency Injection,依赖注入)是IoC的一个别名,其实两者是同一个概念,只是从不同的角度描述罢了(IoC是一种思想,而DI是一种具体的技术实现手段)。

1.2.2 依赖注入实战XML方式

本小节涉及的项目参见第1章项目源代码文件夹下面的“springioc_xml”。下面讲解在IDEA中创建一个基于XML配置方式的Spring项目的典型步骤。

创建一个Maven项目,界面如图1-4所示。

图1-4

pom.xml文件中引入Spring常用依赖。

示例代码1-1 pom.xml(部分代码)

创建空的Spring配置文件。在resources文件夹中创建一个名为applicationContext.xml的文件。命名并无规定,还有其他的常用命名,比如spring-context.xml、beans.xml等。

定义Bean对象。定义UserDao接口,里面包含一个插入用户的方法insertUser。接口代码参见项目源码。定义一个Bean对象,生产该对象并测试该对象内的方法(UserDaoImpl),其中UserDaoImpl实现类的定义代码如下。

示例代码1-2 UserDaoImpl.java

完成UserDao实现对象的注入。依赖注入在这里可以理解为将要生产的对象注入Spring容器中,也就是在spring-context.xml文件利用标签注入,这样就可以让Spring知道要生产的对象是谁。标签写法:<bean id="唯一标签" class="需要被创建的目标对象全限定名"/>,示例如下。

示例代码1-3 applicationContext.xml

调用Spring工厂创建对象。调用Spring工厂API接口ApplicationContext读取配置Spring核心配置文件并创建工厂对象,测试类代码如下。

示例代码1-4 UserDaoImplTest.java

注意,要使用以上测试类,就必须引入junit的依赖,具体依赖代码如下。

我们在项目开发中会进行逻辑分层,一般情况下在Dao层的上一层会定义业务层来对其进行调用,即Service层。Service层依赖Dao对象。

首先,定义UserService接口,代码如下。

示例代码1-5 UserService.java

定义接口实现类UserServiceImpl,代码如下。

示例代码1-6 UserServiceImpl.java

我们可以看到,在Service中定义了userDao属性,并定义了Setter方法,这在Spring配置文件中通过依赖注入方式实现,配置文件修改如下。

示例代码1-7 applicationContext.xml

下面定义一个测试方法,获取UserService对象,并调用insertUser方法。

示例代码1-8 UserDaoImplTest.java(部分代码)

关于注入的方式有多种,比如Setter注入、构造函数注入等。上面的案例采用的是基于Setter方法实现的依赖注入,这也是推荐使用的注入方式。

1.2.3 依赖注入过程说明

依赖注入是一个过程,在此过程中对象仅通过构造函数参数、工厂方法的参数或从工厂方法构造或返回对象实例后在对象实例上设置的属性来定义其依赖项。然后容器在创建Bean时注入这些依赖项。

1.基于构造函数的依赖注入

基于构造函数的DI是通过容器调用具有多个参数的构造函数来实现的,每个参数代表一个依赖项。

2.基于Setter的依赖注入

基于Setter的DI是在调用无参数构造函数或无参数静态工厂方法来实例化Bean之后,容器在Bean上调用Setter方法来实现的。

在了解了依赖注入方式之后,我们具体看一下依赖解析过程。

(1)创建容器时,Spring容器验证每个Bean的配置。在实际创建Bean之前,不会设置Bean属性(properties)本身。创建容器时,将创建单例作用域(在Bean作用域中定义)且设置为预实例化(默认)的Bean,否则只有在请求时才会创建Bean。

(2)如果主要使用构造函数注入,就可能会创建无法解析的循环依赖场景。例如,类A需要一个类B通过构造函数注入的实例,而类B需要一个类A通过构造函数注入的实例。如果将Bean配置为类A和类B相互注入,Spring IoC容器将在运行时检测这个循环引用,并抛出一个BeanCurrentlyInCreationException异常。可以使用Setter注入配置循环依赖项。与典型的情况(没有循环依赖关系)不同,Bean A和Bean B之间的循环依赖关系迫使一个Bean在完全初始化之前被注入另一个Bean中(典型的先有鸡还是先有蛋的场景)。

(3)Spring在容器加载时检测配置问题,例如对不存在的Bean和循环依赖项的引用。在实际创建Bean时,Spring会尽可能晚地设置属性和解析依赖项。

(4)如果不存在循环依赖项,那么当一个或多个协作Bean被注入依赖Bean时,每个协作Bean在被注入依赖Bean之前都是完全配置的。这意味着,如果Bean A依赖于Bean B,那么Spring IoC容器在调用Bean A上的Setter方法之前完全配置了Bean B。换句话说,Bean是实例化的(如果它不是预先实例化的Singleton),它的依赖是设置的,并调用相关的生命周期方法(例如配置的init方法或InitializingBean回调方法)。

3.懒初始化Bean(延迟初始化Bean)

默认情况下,ApplicationContext implementations将创建和配置所有的单例Bean作为初始化过程的一部分。通常,这种预实例化是可取的,因为配置或周围环境中的错误会被立即发现。如果不希望这种行为,则可以通过将Bean定义标记为延迟初始化来防止单例Bean的预实例化。延迟初始化的Bean告诉IoC容器在首次请求时(而不是在启动时)创建一个Bean实例。比如:

    <bean id="lazy" class="com.something.ExpensiveToCreateBean"
lazy-init="true"/>

4.方法注入

在大多数应用程序场景中,容器中的大多数Bean都是单例的。当一个单例Bean需要与另一个单例Bean协作或者一个非单例Bean需要与另一个非单例Bean协作时,通常通过将一个Bean定义为另一个Bean的属性来处理依赖关系。当Bean生命周期不同时,就会出现一个问题。假设singleton Bean A需要使用非singleton(prototype)Bean B,那么,在程序中容器只会创建一次singleton Bean A,因此只有一次机会设置属性,容器不能在每次需要Bean A时都为它提供一个新的Bean B实例。解决的办法是放弃一些控制反转,通过实现ApplicationContextAware接口,并在每次Bean A需要时调用容器的getBean("B")来请求Bean B实例(通常是一个新的)。

1.2.4 Spring容器中的Bean作用域和对象初始化

开发者主要是使用Spring框架做两件事:①开发Bean;②配置Bean。对于Spring框架来说,它要做的就是根据配置文件来创建Bean实例,并调用Bean实例的方法完成“依赖注入”,这就是所谓的IoC本质。

当通过Spring容器创建一个Bean实例时,不仅可以完成Bean实例的实例化,还可以为Bean指定特定的作用域。容器中Bean的作用域有很多种,Spring支持如下五种作用域。

(1)singleton:单例模式,在整个Spring IoC容器中,singleton作用域的Bean将只生成一个实例。

只管理一个单例Bean的共享实例,所有对ID或ID与该Bean定义匹配的Bean的请求,都会导致Spring容器返回一个特定的Bean实例。换句话说,当你定义一个Bean并且它的作用域是一个singleton时,Spring IoC容器正好创建了该Bean所定义对象的一个实例。这个单实例存储在这样的单例Bean缓存中,该命名Bean的所有后续请求和引用都返回缓存的对象。单例作用域是Spring中的默认作用域,其工作模式如图1-5所示。

图1-5

(2)prototype:每次通过容器的getBean()方法获取prototype作用域的Bean时都将产生一个新的Bean实例。

每次请求特定Bean时都创建一个新的Bean实例。通常,应将prototype作用域用于所有有状态Bean,将单例作用域用于无状态Bean。与其他作用域不同,Spring不管理prototype Bean的完整生命周期。容器实例化、配置、以其他方式组装prototype对象并将其交给客户端,而不再记录该prototype实例。因此,尽管初始化生命周期回调方法在所有对象上都被调用而不管其作用域如何,但对于prototype作用域,配置的销毁生命周期回调不会被调用。客户端代码必须清理prototype作用域对象并释放prototype Bean所拥有的昂贵资源。要让Spring容器释放prototype作用域Bean所拥有的资源,可尝试使用自定义Bean后处理器,该处理器保存对需要清理的Bean的引用,该作用域工作模式如图1-6所示。

图1-6

(3)request:对于一次HTTP请求,request作用域的Bean将只生成一个实例,这意味着在同一次HTTP请求内,程序每次请求该Bean得到的都是同一个实例。只有在Web应用中使用Spring时,该作用域才真正有效。

(4)session:该作用域将Bean的定义限制为HTTP会话,只在web-aware Spring ApplicationContext的上下文中有效。

(5)global session:每个全局的HTTP Session对应一个Bean实例。在典型的情况下,仅在使用portlet context的时候有效,同样只在Web应用中有效。

如果不指定Bean的作用域,Spring默认使用singleton作用域。prototype作用域的Bean的创建、销毁代价比较大。而singleton作用域的Bean实例一旦创建成功,就可以重复使用。因此,应该尽量避免将Bean设置成prototype作用域。

Spring容器对象实例化也有几种方法,可以通过构造函数、静态工厂、实例工厂方法等来实现。

(1)用构造函数实例化

当你使用构造函数方法创建Bean时,所有普通类都可以由Spring使用并与之兼容。也就是说,正在开发的类不需要实现任何特定的接口或以特定的方式进行编码,只需指定Bean类就足够了。根据你对特定Bean使用的IoC类型,你可能需要一个默认(空)构造函数。Spring IoC容器几乎可以管理你希望它管理的任何类,不仅限于管理真正的JavaBean。使用基于XML的配置元数据,你可以如下指定Bean类:

     <bean id="exampleBean" class="examples.ExampleBean"/>

(2)静态工厂方法实例化

在定义使用静态工厂方法创建的Bean时,使用class属性指定包含静态工厂方法的类,使用名为factory method的属性指定工厂方法本身的名称。你应该能够调用此方法(如后文所述,使用可选参数)并返回一个活动对象,该对象随后将被视为是通过构造函数创建的。这种Bean定义的一个用途是在遗留代码中调用静态工厂。

以下Bean定义指定通过调用工厂方法创建Bean,其中没有指定返回对象的类型(类),只指定包含工厂方法的类。在本例中,createInstance方法必须是静态方法。

    <bean id="clientService" class="examples.ClientService"
factory-method="createInstance"/>

(3)使用实例工厂方法实例化

与通过静态工厂方法进行实例化类似,使用实例工厂方法的实例化从容器中调用现有Bean的非静态方法来创建新Bean。一个工厂可以包含一个以上的工厂方法。工厂Bean本身可以通过依赖注入进行管理和配置。

(4)Java注解方式实例化

Java配置是Spring 4.x之后推荐的配置方式,可以完全替代XML配置,主要包含两个注解,分别是@Configuration和@Bean。Spring的Java配置方式是通过@Configuration和@Bean注解实现的:@Configuration作用于类上,相当于一个XML配置文件;@Bean作用于方法上,相当于XML配置中的<bean>。

1.2.5 依赖注入实战Java注解配置方式

1.2.2小节中我们使用XML方式进行Bean的配置,但XML方式配置过于复杂且不好维护,目前大多数企业在开发中采用的都是基于Java注解的方式,具体项目参见本章项目源码文件夹下面的“springioc_anno”。

首先需要编写SpringConfig,用于实例化Spring容器,然后打上@Configuration注解,同时打上@ComponentScan配置扫描的包。

这里采用的Bean对象依然是1.2.2小节中的UserDao和UserService对象,一般做法是在两个类中打上相应的注解,而在Spring配置类中对其进行扫描。

一般情况下按照不同业务逻辑含义,不同逻辑层的对象需要对应不同的注解。Dao层使用@Repository注解,Service层使用@Service注解。这两个注解实际上都是集成@Component注解。

如果需要在Spring配置类中注入Bean,则可以使用@Bean注解。@Bean用于向容器中注入对象,如果在UserDao类前面打上@Repository注解,就不用@Bean方式。

示例代码1-9 SpringConfig.java

注意

方法名是返回对象的名字,因此一般不带get,也就是上述放入Spring容器的Bean的name为getUserDao。

UserService实现类需要打上@Service注解,并在属性userDao上面打上@Autowire注解,实现二者的注入关系,将原本在XML中的配置集中到Java代码中,减少了维护成本。具体UserServiceImpl.java代码如下。

示例代码1-10 UserServiceImpl.java

编写测试方法,用于启动Spring容器。

示例代码1-11 Application.java i9j1ls56/WmlnY0hEDBWis7VY0DMgyaYQOtsk5FqY4lyL4uUHkUJUEhEd52sZf85

点击中间区域
呼出菜单
上一章
目录
下一章
×