SpringIOC

什么是IoC容器

IOC(Inverse of Control):控制反转,将对象对象创建的权力反转给Spring DI(Dependency Injection):依赖注入,意思对象中的内置对象是通过注入的方式(构造方法、set方法、直接赋值)进行创建

IoC是一种编程思想,是DI的基础,Spring的IoC就是通过DI实现的。在Spring中,由SpringIoC管理的对象称作Bean,Bean是由IOC容器实例化、组装、管理的对象。Bean之间拥有依赖关系,可以通过配置相关元数据(XML、Java注解、Java代码)表明Bean之间的关系。

spring中有关IoC容器的包全在模块spring-core下的org.springframework.beansorg.springframework.context包下。其中BeanFactory接口是关于Bean管理的最高层抽象,涵盖了常用的基本功能;而ApplicationContextBeanFactory的子接口,在BeanFactory的基础上,添加了以下几种特殊功能:

  • 更容易与Spring AOP 集成

  • 消息资源处理(用于国际化)

  • Event publication

  • 特定于应用程序层的上下文,例如WebApplicationContext,用于Web应用程序

IoC容器概览

ApplicationContext接口用于表示IOC容器,它负责实例化、配置、组装Bean。它通过读取配置的元数据获取有关实例化、配置、组装对象的指令。同时也允许你定义组成应用程序的Bean之间依赖关系。

Spring针对ApplicationContext接口提供了几种实现,比如:ClassPathXmlApplicationContextFileSystemXmlApplicationContext。XML是传统定义Bean的形式,如果想使用注解,可以通过少量的XML开启注解使用。

配置元数据

Spring通过元数据配置Bean:

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">
    <!--id区别其他bean的唯一标志-->
    <!--class是指此对象的类型的权限定名-->
    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions go here -->

</beans>

通过ClassPathXmlApplicationContext 加载xml元数据实例化IoC容器:

注解形式则是在含有注解@Configuration的类中使用注解@Bean

基于多个xml配置元数据

通常我们将不同的模块或者逻辑层定义在不同的xml文件中,然后通过一个或者多个<import />元素从其他xml中加载bean定义:

注意:不推荐使用../引用上层目录的资源文件,特别是classpath:../

Groovy Bean定义DSL

类似Java定义Bean,不过更为便捷,编写db.groovy:

使用GenericGroovyApplicationContext加载Groovy元数据:

使用IoC容器

ApplicationContext是高级工厂接口,能够维护不同bean和与其他bean的关系;可以通过T getBean(String name, Class<T> requiredType)从IoC中获取一个bean实例:

对于Groovy配置:

还有一种比较灵活的方式GenericApplicationContext, 通过 Reader Delegates  (读取委托),既可以加载XML配置也可以加载Groovy配置:

Bean 概览

IoC容器配置负责管理一个或者多个Bean,由用户提供的元数据创建的。在IoC容器内,这些Bean实际被定义为BeanDefinition对象,其中包含自己的元数据:

  • Class name: bean的类型

  • Bean behavioral configuration elements: 表示bean在容器中的行为方式(生命周期回调、scope等)

  • 应用Bean执行其工作所需要的其他Bean,这些引用也称为依赖项

  • 配置的对象属性,比如池对象的池大小

这些元数据转换为构成每个bean定义的一组属性,这些属性包含:

Property
说明

Class

Name

Scope

Constructor arguments

Properties

Autowiring mode

Lazy initialization mode

Initialization method

Destruction method

除了通过元数据定义来定义并加载bean到IoC容器之外,还提供了一种容器外部的bean的注册方式:

Naming Beans(对Bean命名)

每个Bean都含有一个或者多个标识符,这些标识符在IoC在IoC中必须唯一。通常情况下,一个Bean只有一个标识符,但是如果需要多个标识符,可以通过 aliases 别名视为额外的标识符。

在基于XML配置的元数据中,可以使用id与name属性或者都使用来作为一个bean的标识符。通常,指定id属性为Bean设置id,比如'myBean', 'someService'等。如果要为Bean引入其他别名(Alias),你可以指定name属性,并且name属性可以指定多个,多个之间可以使用分隔符空格;, 分割:

不需要为每个bean配置name或者id,spring会默认为配置文件中的bean添加一个唯一标识。但是如果你需要使用名称引用这个bean,那么你必须为此bean定义name。一般情况下,自动装配(Autowried)的或者是Bean内部的Bean对象,通常不设置name就使用。

Bean的命名

与Java类似,Bean的命名通常是以类名的小写字母开头定义,比如类OrderService,bean的名称就推荐设置为orderService。而没有定义bean name的bean,在被IoC扫描到后,默认也会以上述规则命名。如果是两个大写字母开头的类,例如HTTPService,那么他的默认名称就是用类名: HTTPService

定义别名(Alias)

使用name或者id定义bean的名称并不总是够用的,也许需要在其他地方使用别名定义bean。比如在大型系统中,每个子系统都想使用自己的别名定义这个bean,此时就可以借助alias标签:

如果是注解方式,可以使用@Bean

Instantiating Beans(实例化bean)

Bean的定义实际就是创建一个或者多个bean的配方(recipe),当容器被访问时,容器会从配方中寻找指定名称的bean,并使用此bean的元数据配置来创建实际对象。

如果你使用xml配置元数据,则需要在<bean/>元素的class属性中指定元素类型。这个属性将会对应BeanDefinitionClass属性。

在spring IoC中,创建实例可以大致分为三种方式:

  • 构造器(Constructor)

  • 静态工厂(Static Factory Method)

  • 实例化工厂(Instance Factory Method)

构造方式

当您通过构造方法创建bean,如果不指定构造参数,就需要提供一个默认的空构造:

静态工厂

Spring 还支持使用静态工厂方法来创建的bean,需要使用属性class来指定工厂类,factory-method指定工厂方法(必须是静态方法):

关于TestServiceImpl:

实例工厂

设置实例工厂初始化方式,首先初始化工厂类bean,使用factory-bean属性指定容器的bean的名称,并且属性class必须为空:

Dependencies(bean的依赖)

企业级的应用程序一般不会只包含单个对象,即使是最简单的应用程序也会由多个JavaBean相互依赖工作,Bean之间不可避免的存在依赖关系。

依赖注入

依赖项注入(Dependency injection, DI)是一个过程,在这个过程中,对象仅通过构造函数参数、工厂方法参数或从工厂方法构造或返回对象实例后在对象实例上设置的属性来定义它们的依赖项(即与它们一起工作的其他对象)。

使用DI原则,代码更简洁,当对象具有依赖关系时,解耦更有效。对象不查找其依赖项,也不知道依赖项的位置或类。因此,您的类变得更容易测试,特别是当依赖关系位于接口或抽象基类上时,这允许在单元测试中使用存根或模拟实现。

DI存在两种主要变体:

  • 基于构造函数的依赖项注入

  • 基于setter的依赖项注入

基于构造函数的注入

基于构造函数的DI是由容器调用一个构造函数来完成的,该构造函数有许多参数,每个参数表示一个依赖项。这个方式与调用具有特定参数的静态工厂来构造bean几乎是等价的。注入形式如下:

构造参数解析匹配

构造参数的解析是通过匹配参数类型进行匹配然后注入的。如果bean定义的构造函数参数中不存在潜在的歧义,则在bean定义中定义构造函数参数的顺序与在实例化bean时将这些参数提供给适当构造函数的顺序相同。

有如下代码:

如果ThingTwoThingTree不存在潜在的依赖关系,则就不存在潜在的歧义。

当引用另一个bean时,类型是已知的,可以进行匹配(就像前面的例子一样)。当使用简单类型时,例如<value>true</value>, Spring无法确定值的类型,因此在没有帮助的情况下无法按类型匹配。

有如下代码:

Spring是无法将100stringxxxx匹配到指定的位置的。

Constructor argument type matching (构造参数类型匹配)

在前面的例子中,如果使用type属性显示指定构造参数类型,那么容器就可以与简单类型进行匹配了:

Constructor argument index (构造参数索引)

可以使用index属性显示指定构造参数的索引:

Constructor argument name(构造参数名称)

也可以使用构造函数的参数名称消除歧义:

注意:编译时,需要开启 debug flag,即勾选IDEAsetting->Build->Compiler->Java Compiler->Generate debugging info选项,否则编译后的class没有参数名称,全是var1var2... 这样Spring就无法与构造器参数映射

基于setter方法的注入

基于setter的DI注入是在spring调用了bean的构造函数或者静态/实例工厂实例化bean之后,通过容器调用bean上的setter方法来完成的。

Application为它管理的bean支持基于构造函数和setter的DI,并且支持同时使用。可以使用bean配置的形式配置依赖项,然后使用ref 引用,还可以与PropertyEditor实例结合使用,将属性转换为另一种格式。但是大多数情况下都不适用PropertyEditor,而是使用xml bean定义,或者使用注解@Component@Controller@Configuration类中的@Bean方法来DI。然后,这些对象源在内部转换为BeanDefinition的实例,并用于加载整个Spring IoC容器实例。

构造DI与SetterDI的比较

因为构造DI与SetterDI可以混合使用,所以将构造函数设置为强制依赖项,将Setter方式依赖设置为可选依赖项比较好。虽然,Spring还提供了一种注解@Required可以使属性称为必须的依赖项,但是并不推荐这种方式。

Spring团队通常提倡构造函数注入,因为它允许将应用程序组件定义为 final 对象,以此确保依赖项不为null。

SetterDI应该只用于可选的依赖项,这些依赖项在类中需要配置合理的默认值。否则,在使用前必须使用非空检查。setter注入的一个好处是,setter方法可以使此类稍后注入或者重新注入。

依赖的解析过程

容器执行bean依赖解析过程如下:

  • ApplicationContext的创建与初始化是为了描述所有bean的元数据

  • 配置元数据可以使用XMLAnnonation

  • 每个bean的依赖关系都以属性、构造函数参数、静态工厂方法参数表示

  • 这些依赖项会在bean实际创建时,提供给bean

  • 每个属性或构造函数参数都是要设置实际值(value)、或者是其他bean引用(ref)

  • 值的每个属性或构造函数参数都从其指定的格式转换为该属性或构造函数参数的实际类型(构造参数解析匹配)

  • 默认情况下,Spring可以将以字符串格式提供的值转换为所有内置类型,比如int、long、string、boolean等等

在创建容器时,Spring容器验证每个bean的配置,但是,在实际创建bean之前不会设置bean属性(属性是指set的属性,不包含构造参数)本身。当bean是一个单例作用域的bean时,实例化后会将属性设置为预实例化(pre-instantiated, 默认值,new完没有set属性;构造参数是无法预实例化的),然后创初始化依赖项,并设置到单例bean中。如果不是单例bean,那么只有bean被请求时才会创建bean。当bean的依赖项以及依赖项的依赖项..被创建和分配时,bean的创建可能会创建bean图(a graph of beans)。

Circular dependencies 循环依赖

如果主要使用构造函数注入,则可能创建无法解析的循环依赖关系场景。

例如: 类A需要一个类B通过构造函数注入的实例,而类B需要一个类A通过构造函数注入的实例。如果将bean配置为类A和类B相互注入,Spring IoC容器将在运行时检测到这个循环引用,并抛出一个BeanCurrentlyInCreationException异常。

解决这种问题(先有鸡还是先有蛋),有两种方案:

  • 更改类源码,只有一方使用setter注入,另一方使用构造注入

  • 避免构造注入,只使用setter注入

不提供方式二,但是仍然可以使用

加载bean的可靠机制

Spring对bean的加载有一套可靠的检测机制,当容器加载时会检测配置问题(比如,检测配置文件是否有循环依赖)。并且在实际创建Bean时,会尽可能晚设置属性和解析依赖项,所以当创建对象或者依赖项时出现异常情况时,有可能只有等到对象被访问后才会抛出异常,这种情况单例作用域的bean是不会出现的;这也是为什么默认的bean会被定义为单例作用域的原因。这种方式牺牲了创建前期的时间与内存,让ApplicationContext在创建时发现问题,而不是等到bean被访问时才发现。

当然,此行为可以被覆盖,以便单例bean延迟加载,而不是预先实例化。

如果对象之间不存在循环依赖关系,当一个或多个协作bean被注入到一个依赖bean中时,这些bean在注入bean之前都会被配置好。这意味着,如果BeanA依赖BeanB,IoC在调用A的setter方法之前会完全配置BeanB;换句话说,bean被实例后(如果不是预先实例化的关系),设置依赖关系,并调用相关的生命周期方法(比如init方法)。

DI Example

下面是基于xml配置的实例代码,setter注入的方式演示:

静态工厂

依赖配置详情

上一小结中已经说明,可以将bean的属性和构造参数定义为其他bean的引用。Spring基于xml的配置元数据支持<property/><constructor-arg/>子元素。

直接值(Straight Values)

<property />元素的value属性将属性或构造函数参数指定为人类可读的字符串表示形式。 Spring的转换服务(conversion service)用于将这些值从String转换为属性或参数的实际类型。以下示例显示了要设置的各种值:

下面可以使用p命名空间使用更简介的配置:

你可以配置java.util.Properties实例,如下:

Spring 会将 <value/> 中的数据转换为java.util.Properties 实例,这是一个比较便捷的方式,这也是Spring团队喜欢使用<value/>标签而不是value属性的原因。

idref属性

idref元素用来将容器内其它bean的id传给<constructor-arg/><property/>元素,同时提供错误验证功能。

上面的定义与下面完全等价:

即将theTargetNamebean的name即"theTargetName"赋值给client的targetName属性。

第一种方式比第二种方式更可取,因为使用idref标记可以让容器在部署时验证引用命名bean是否存在,而第二种方式则不会去验证,当你的应用程序更具名称实例化对象时才会发现错误。

除了,如果被引用的bean在同一个xml文件中,且bean的名字就是bean的id,除了可以使用,此属性允许xml解析器在解析XML的时候对引用的bean进行验证。

而value方式,传给client bean的targetName属性值并没有被验证。任何的输入错误仅在client bean实际实例化时才会被发现(可能伴随着致命的错误)。

在4.0 bean XSD中不再支持idref元素上的local属性。

<idref/>的典型应用(至少在Spring 2.0之前的版本中)是ProxyFactoryBean 中定义AOP拦截器配置。在指定拦截器名称时使用<idref/>元素可以防止拼写错误

ref属性

ref元素是<constructor-arg/><property/>定义元素中的最后一个元素,他的作用可以将bean的某个属性值设置为其他bean的引用。所引用的bean在被依赖注入之前,需要进行初始化,并设置属性。所有引用最终都指向同一对象。

通过<ref />标记的bean属性指定target bean是最常用的形式,并允许创建对同一容器或父容器中的任何bean的引用,而不管它是否在同一XML文件中。bean属性的值可以与目标bean的id属性或者name属性相同。使用ref:

通过parent属性创建当前容器的父容器的bean的引用,并使用这个引用指定目标bean。parent属性的值可以与目标bean的id属性值与name属性值相同。

当你的容器具有层次结构并且希望将现有bean包装在父容器中

InnerBean

可以在<property/>或者<constructor-arg/>中直接定义<bean/>,这个bean类似一个java中的匿名内部类:

内部bean无需定义idname,即使制定了这两个属性,容器也不会使用他们作为bean的标识符。容器在创建时,会忽略标识符,因为内部bean是匿名的,并且总是outer bean(包含内部bean的bean)创建的。因为内名,内部bean是无法独立的访问,更不可能作为其他bean的依赖。

Collections

Spring还提供了<list/>, <set/>, <map/>, 以及<props/>标签元素,分别表示Java中的 List, Set, Map, 与Properties集合类型。例子如下:

如下标签都也可以作为keyvalueset属性的值:

集合合并

Spring容器还支持集合的合并。可以在父Bean中定义<list/>, <map/>, <set/> 或者<props/>元素;然后子Bean从父Bean中继承集合或者是覆盖集合中的值。也就是说,子集合的值,是合并父集合与子集合元素的结果。举例:

合并后集合的结果为:

这种集合合并的行为特性,不仅在<property/>生效,同样也在<list/>, <map/>, 和<set/>上生效。特殊的,<list/>集合同样也会维护他的次序,父列表的值都会位于子列表之前。

Null和空字符串

当xml中属性为空时,Spring会将这个空认为空字符串,如下:

如果想为属性设置为null值,可以使用<null/>

p-namespace

p-namespace允许使用bean元素属性,而不是嵌套的<property/>属性来设置属性的值。当然,这两种方式可以同时使用。

p-namespace 不如标准的xml方式灵活,声明属性引用的格式与以Ref结尾的属性冲突,而标准XML格式不冲突

c-namespace

spring3.1 引入了 c-namespace,与p命名空间类似,c命名空间用于配置构造器参数。

如果构造名称不可用,可以使用构造参数索引:

因为xml属性名称不可以以数字开头,所以在索引前加了一个_下划线。

建议使用名称表示法,而不是索引表示法,因为构造函数解析机制在匹配参数方面比较高效。

复合属性设置

something bean中含有属性fred,如果可以保证fred不为空、fred的bob属性不为空,就可以这么使用;这样讲设置fred的bob的sammy属性的值。

depends-on

如果BeanA是BeanB的依赖项,通常BeanA会被设置为BeanB的属性。在xml中,通常使用ref指定属性的引用。但是有些情况下,Bean之间的依赖不需要设置的这么直接明显,比如在数据库驱动加载。这类依赖可以通过depends-on来解决:

如果beanOne要对多个bean进行依赖,可以在depends-on指定多个bean的name,使用逗号、空格或是分号隔开。

懒加载bean

默认情况下,ApplicationContext会在初始化过程中急切创建单例bean。通常情况下,这种方式会立即发现错误。当不需要这种机制时,可以通过对bean设置懒加载来防止单例bean的立即初始化。延迟初始化的bean,会在第一次被请求时创建bean实例。在xml中,可以通过<bean/>lazy-init属性控制,如下:

但是如果lazy bean被其他单例bean依赖,那么,懒加载就会失效。

除此之外,还可以通过元素<beans/>default-lazy-init属性控制容器级别的懒加载:

Autowired

Spring 容器可以自动连接Bean之间的依赖关系。Spring通过检查ApplicationContext中的内容,可以让Spring自动解析解析注入Bean的依赖。自动装配具有以下几个优点:

  1. 减少属性和构造器的定义

  2. 自动装配可以随着对象的发展自动更新配置;如果需要向类添加依赖项,则可以自动满足该依赖项,而不需要修改配置

当使用xml作为元数据时,可以使用<bean/>autowire属性为bean指定注入模式,共有四种注入方式:

模式
解释

no

默认,不自动装配。Bean的引用必须由ref元素定义。对于复杂的依赖关系,不建议更改默认配置,因为显示指定依赖项可以提供清晰的依赖关系。

byName

通过属性名自动注入,spring寻找与属性名称相同的bean。

byType

如果容器中只有一个属性类型的Bean,则允许自动调用属性。如果存在多个此类型的Bean,则会抛出异常,这样就不能使用byType了。

constructor

类似于byType,但是用于构造函数。如果容器中没有构造函数参数类型的bean,则会抛出异常

自动装配的局限性和缺点

自动装配在项目中一致使用最好。

  • 属性和构造函数参数的显示依赖总是覆盖自动装配

  • 不能自动生成简单的属性,比如字符串、简单类型

  • 自动装配不如显示连接准确,spring对象之间的关系将不会明确的记录下来

  • 可能无法为可能从Spring容器生成文档的工具提供连线信息

Bean定义的继承

bean定义可以包含很多配置信息,包括构造函数参数、属性值和特定于容器的信息,比如初始化方法、静态工厂方法名称等等。子Bean定义可以从已经定义的父Bean继承相关配置信息,然后根据需要覆盖一些值或者添加一些值。使用父Bean与子Bean模式定义高度完成配置的复用,是一种模板的表现形式。

如果以编程方式使用ApplicationContext接口去创建子Bean,那你需要类ChildBeanDefinition:

大部分情况下,都是使用xml的配置方式,可以进行如下配置:

注意,如果要实现定义的继承,那么,必须保证子Bean和父Bean的类型是兼容的,也就是说,子Bean的属性集合包含父Bean的属性集合。

如果父Bean定义了class,并且子bean没有定义class,那么子Bean就会继承父Bean的class。而如果父Bean没有定义class,那么子Bean必须定义class属性,此时父bean就是一个纯模板bean,仅仅充当子bean的父定义。

子bean会继承父bean的作用域(scope)、构造函数参数值、属性、方法,还可以添加新的属性。子bean重新指定这些属性,都会覆盖相应的父设置。而其余的设置,比如依赖、自动装配模式(autowrire mode)、依赖检查、单例、惰性初始化等,总是以子类为准

前面的示例,使用了属性abstract=true显示定义了父Bean为抽象类型。如果父Bean定义没有指定class,那么必须显示的将父bean标记为抽象:

如果父Bean已经被定义为abstract,那么父Bean不能单独实例化,仅仅只能作为子类的模板。并且,容器内部预实例化preInstantiateSingletons()方法会忽略抽象bean的定义。

默认情况下,ApplicationContext与实例化所有单例。如果一个父Bean定义只打算作为模板,那么你就必须保证abstract=true,否则应用ApplicationContext会视图进行与实例化他。

最后更新于

这有帮助吗?