最近想系统的学习一下Spring的相关知识,在这里分享一下。也希望能帮助那些对英文技术文档阅读比较困难的朋友。
接下来会持续更新,如果文中有解释的不当之处,欢迎指正,谢谢!~
2018/11/2 ,在翻译的过程中,我发现有些地方英文表达的有些冗余,我可能不会直译,请注意。
英文文档
1.1. 介绍IoC容器和Bean
本章涵盖了 SpringFramwork 中 IoC (控制反转)的实现原理。IoC 也被叫做 DI (依赖注入),它是一个对象定义依赖关系的过程,也就是对象只能通过构造函数参数,工厂方法参数,或者(对象通过new或者工厂方法返回以后)设置属性来定义与其他对象的依赖关系,然后容器在创建Bean时注入这些依赖项。这个过程本质是反转的,因此得名控制反转,其含义是bean自身通过类的构造函数控制对象的初始化或者依赖发现,还是通过像 Service Locator Pattern机制。(关于什么服务定位模式,可以阅读这篇博客Service Locator Pattern)。
org.springframework.beans
和 org.springframework.context
是 IoC 容器的基础包,BeanFactory
接口提供了高级配置机制,可以管理任何类型的对象。ApplicationContext
是 BeanFactory
的子接口,它添加了易与 Spring AOP 集成,消息资源处理(如:国际化),事件发布以及应用层特定的的contexts,比如web应用的WebApplicationContext
等功能。
简而言之,BeanFactory
接口提供了配置框架和基本功能,ApplicationContext
添加了企业级应用特用的功能点。ApplicationContext
是 BeanFactory
一个完整的超集,也是在本章中专门用于解释 IoC Container的contexts,有关使用 BeanFactory
而不是使用 ApplicationContext
的更多信息,请参考 Bean Factory。
在 Spring 中,Spring IoC Container 管理了所有组成应用主干的对象,这些对象叫做 Beans,Bean 要么是由 Spring IoC Container 实例化、装载以及管理的对象,要么就是一个应用中简单的对象。Beans,以及它们之间的依赖关系是在容器使用的配置元数据中配置的。
总结:
- 什么是控制反转。
-
BeanFactory
和ApplicationContext
的关系。
1.2. 容器概述
org.springframework.context.ApplicationContext
接口表示 Spring IoC Container,负责实例化、配置以及装载 beans。容器通过读取配置元数据实例始化、配置以及装载对象。配置有三种形式,分别是:XML、Java annotations 或者 Java code,它允许你定义应用程序的对象以及这些对象的相互依赖关系。
Spring 提供了 ApplicationContext
接口的几个开箱即用的实现,在独立应用中,通常会创建 ClassPathXmlApplicationContext
或 FileSystemXmlApplicationContext
的实例。虽然 XML 一直是定义配置的传统方式,但是你也可以通过提供少量的 XML 配置来启用 Java annoations 或 Java Code 的配置方式。
大多数场景下,不需要显示创建 Spring IoC Container 的实例,比如:Web 应用场景中,web.xml 文件用简单的大约8行描述就足够。如果你使用 Spring Tool Suite,那么这个配置将会很容易创建,只需要几次键盘鼠标操作。
下图是 Spring 工作原理的高级视图,应用程序类和配置元数据组合起来,在 ApplicationContext
创建和初始化以后,就有一个完整的可执行系统或应用程序。
1.2.1. 配置元数据
如上图所示,Spring IoC Container 需要一份配置元数据,由应用程序开发人员编写用来告诉容器如何实例化、配置和装载对象。
配置元数据传统方式是由简单、直观的XML格式配置,本章中阐述Spring IoC Container 的关键概念和特性都是用这种方式。
注意:基于XML并不是配置的唯一途径。Spring IoC容器与配置文件完全解耦。现在,许多开发者在他们的Spring应用中选择使用基于Java的配置(后续章节会介绍此部分内容)。
关于配置元数据的其他形式,请参考:
- Annotation-based configuration:Spring 2.5引入的基于注解配置的元数据。
- Java-based configuration:Spring 3.0开始,Spring JavaConfig提供的许多特性已经成为了Spring Framework的核心。你可以使用Java而不是XML文件来定义程序类外部的bean。要使用这些新特性,可以参考
@Configuration
,@Bean
,@Import
和@DependsOn
等注解。
Spring 配置至少由一个,通常有多个容器管理的 bean 组成,XML 通过
元素来配置 bean,其父元素,也是顶级元素是
。基于 Java 配置 bean 通常是在 @Configuration
标注的类中,用 @Bean
标注的方法,表示这个方法会返回一个 bean。
这些 bean 的定义对应组成应用程序的实际对象,通常你可以定义服务层对象,数据访问层对象,表示层对象(Struct Action 实例),底层对象(Hibrenate SessionFactories),JMS Queues等等,但不需要配置一些细粒度的域对象,因为这些对象通常是由 DAOs 和业务逻辑创建和加载。然而,你也可以通过 Spring`s integration with AspectJ 来配置在 IoC Container 之外创建的对象,请参考:Using AspectJ to dependency-inject domain objects with Spring.
下面的例子展示了基于 XML 配置的基本结构:
id 属性是一个字符串,唯一标识一个 bean,可以被其他对象引用。class 属性定义 bean 的类型,使用类的完全限定名。关于依赖引用配置,请参考:Dependencies
1.2.2. 初始化容器
初始化一个容器很简单,给 ApplicationContext
构造函数传递一个或多个资源路径,容器也可以加载各种外部资源(文件系统、Java 类路径等)。
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml","daos.xml");
学习了Spring IoC容器之后,你可能会想去知道更多有关于Spring的Resource,像在Resource章节描述的一样,Spring Resource提供了方便的机制来使用URI规则读取InputStream。尤其是,Resource路径被用来构建应用程序context,下面章节会给出具体描述。
下面的例子展示了服务层bean的配置(services.xml):
下面的例子展示了数据接入层bean的配置(daos.xml)
在上面的例子中,服务层由类 PetStoreServiceImpl
以及两个数据接入层的对象 JpaAccountDao
和 JpaItemDao
组成。
元素的 name
属性关联bean对象的属性名称,ref
关联依赖的 bean 的 id
。
基于XML配置的组合使用
将bean的定义按逻辑层或者功能模块拆分到多个XML配置文件中是很有用的。
你可以给 ApplicationContext
构造函数中传递多个路径来加载配置,也可以通过
标签,将多个配置文件组合在一起使用,比如:configuration.xml
在上面的的例子中,外部的 bean 定义是通过:services.xml
,messageSource.xml
和 themeSource.xml
三个文件加载。所有的路径都是 configuration.xml
的相对路径,所以 services.xml
必须在
configuration.xml
的相同目录下或者类路径下,messageSource.xml
和 themeSource.xml
必须在和 configuration.xml
同级目录下的 resources
目录下。正如你所看到的,messageSource.xml
文件路径的前置斜杠被忽略,因为这些路径都是相对的,所以最好使用这种形式。被导入文件的内容,包括顶级元素
通过 Spring Schema 的校验是一个有效的 XML bean 的定义。
引用父目录文件的时候,使用相对路径“../”,这样做是可以的,但是并不推荐这样做。这样做会创建一个相对当前应用程序之外的文件的依赖关系。尤其是,这样引用不推荐用于"classpath:"路径(例如"classpath:../services.xml"),在运行时解析过程中会选择“最近的”类路径的根目录,然后再寻找它的父目录。classPath配置的改变,可能会导致选择不同的、错误的目录。
你也可以使用绝对路径来代替相对路径:例如“file:C:/config/services.xml” 或 “classpath:/config/services.xml”。然而,你可能意识到,这样做会将你的应用程序的配置,耦合到一个特定的位置上。一般不会直接直接配置这种绝对路径,例如,可以使用"${…}"占位符来获得JVM运行时的系统属性。
标签是 beans 命名空间自身提供的特性。在Spring 提供的 XML 命名空间里,还是很多其他的配置特性,例如 context
和 util
等。
1.2.3. 使用容器
ApplicationContext
接口是一个高级工厂接口,它能维护不同 bean 以及它们的依赖的注册表。通过 T getBean(String name,Class
方法,可以获取bean的实例。
ApplicationContext
允许你读取bean 的定义以及访问它们,比如:
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml","daos.xml");
// retireve configured instance
PetStoreService service = context.getBean("petStore",PetStoreService.class);
// use configured instance
List userList = service.getUserNameList();
最灵活的方式是使用 GenericApplicationContext
,它整合了具有代表性的几个 reader
,比如,为 XML 文件准备的 XmlBeanDefinitionReader
:
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml","daos.xml");
context.refresh();
这样同一个 ApplicationContext
可以读取多个不同配置源的 bean 的定义。
你可以通过 getBean
方法获取 bean 的实例,ApplicationContext
接口还提供了一个获取 bean 的实例的方法,不过,理想状态下,你的应用代码中不需要使用它们。实际上,你的应用代码中不会调用 getBean
方法,因此不需用依赖 Spring APIs。例如,Spring与web框架的集成,为各种web框架组件(例如controller,JSF)提供了依赖注入,允许您通过配置声明对特定bean的依赖(例如:autowiring注解)。
总结:
- 容器、Bean、配置元数据之间的关系。
- 如何初始化和使用容器。
- 如何加载多个配置。
- 配置元数据有那些形式。
1.3. Bean概述
Spring IoC Container
管理一个或多个 bean
,这些 bean
是通过你提供给 Container
的配置元数据创建的,例如:XML 形式的
定义。
在 Container
内部,这些 bean
的定义被表示为 BeanDefinition
对象,其中包含了一下元数据(以及其他信息)。
- 类的包限定名,通常是所定义
bean
的具体实现类。 -
bean
的行为配置元素,这些元素声明了bean
在容器中的行为,例如:作用域、生命周期回调函数等等。 - 对其他
bean
的引用,这些引用叫做协作者或者依赖。 - 其他配置项设置新创建的对象,例如:管理连接池的
bean
,可以设置这个bean
的连接数以及池的大小。
这些元数据转化为 BeanDefinition
对象的属性。
Table 1. Bean 定义
属性 | 详述 |
---|---|
class | Instantiating beans |
name | Naming beans |
scope | Bean scopes |
constructor arguments | Dependency Injection |
properties | Dependency Injection |
autowiring mode | Autowiring collaborators |
lazy-initializtion mode | Lazy-initialized beans |
initializtion method | Initialization callbacks |
desturction method | Destruction callbacks |
除了通过 bean 定义的方式创建 bean 以外,ApplicationContext
也允许注册在容器外用户创建的对象,通过调用 ApplicationContext
接口的 getBeanFactory
方法,返回一个实现了 BeanFactory
接口的 DefaultListableBeanFactory
对象,这个对象提供了两个注册方法 registerSingleton(...)
和 registerBeanDefinition(...)
。 然而,通常是通过配置元数据定义需要创建的 bean
。
Bean元数据和手动提供的单例需要尽早注册,以便容器在自动注入和其他自省步骤期间正确地推断它们。
虽然在一定程度上支持覆盖现有的元数据和现有的单例,但是在运行时注册新bean(与对工厂的实时访问同时进行)并没有得到官方的支持,并且可能导致bean容器中的并发访问异常和/或不一致状态。
1.3.1. beans 命名
每个 bean
可以有一个或多个标识符,这些标识符必须在 bean
所在的容器中唯一。通过一个 bean
只有一个标识符,其他标识符可以作为 bean
的别名。
基于 XML 配置的元数据中,使用 id
、name
来指定 bean
的标识符。通常这些标识符由数字和字母组成(myBean
,fooService
等等),但是也可以使用特殊字符。通过 name
属性给 bean
指定一个或多个别名,用逗号、分号或者空格分割。在 Spring 3.1 之前的版本中, id
属性被定义为 xsd:ID
类型,限制了一些字符的使用,截止 Spring 3.1,它被定义为 xsd:string
类型,虽然不在需要 XML 解析器校验唯一性了,但是 id
在容器内仍然强制要求必须唯一。
可以不用显示的指定 bean
的 id
或者 name
,容器会给 bean
生成一个唯一的名称,但是如果需要通过名称引用 (ref
或者 Service Locator Pattern
)bean
,就必须手动提供一个名称。如果被用作 inner beans 或者 autowiring collaborators.,可以不用手动提供名称。
Bean 命名规范
bean
的命名规则和java
中类的字段的命名规则一致,使用首字母小写的驼峰命名,比如:accountManager
,accountService
,等等。
在
classPath
下扫描组件时,Spring
会给未命名的bean生成名称,遵循上面说的规则:实质上就是取简单类型并将首字母小写。然而在特殊情况下,如果类名的前两个字符都是大写的话,Spring会采用原始的命名,不会做改动,具体的逻辑可以参考Spring
命名的方法java.beans.Introspector.decapitalize
。
定义之外给 bean
引入别名
在 bean
定义时,可以通过设置一个 id
和 多个name
给bean 命名,这些名称都是指向同一个 bean
的等价别名,在有些场景下这很有用,比如:应用程序中的每个组件希望自己指定一个 bean
名称来引用通用组件。
在 bean
定义的地方设置所有的别名不总是很适合,有时希望在 bean
定义之外给 bean
设置别名。一个常用的场景是:在一个大型系统中,配置分布在每个子系统中,每个子系统定义他们所需要的 bean
,基于 XML 配置中,可以通过
标签实现。
在这种情况下, 同一个容器中有一个叫做 fromName
的 bean
,通过上面的别名定义,可以通过 toName
去引用这个 bean
。
比如,子系统 A
通过 subsystemA-dataSource
引用 DataSource
,子系统 B
通过 subsystemB-dataSource
引用 DataSource
,由子系统 A
和 子系统 B
组成的应用使用 myApp-dataSource
引用 DataSource
,将下面的代码添加到应用的配置中,将会有三个名称指向同一个对象。
现在每个组件和应用通过一个唯一的名称引用相同的 DataSource
,这个名称也不会和其他定义冲突。
Java-configuration
如果使用 Java-configuration 的形式配置,@Bean 标签可以提供别名
1.3.2. 实例化 beans
bean
定义本质是为了创建一个或多个对象,当需要一个 bean
时,容器会根据配置元数据创建实际对象。
基于 XML 的配置,需要通过
元素的 class
属性指定对象的类型,这个属性通常是必须指定的,在容器内部,它是 BeanDefinition
实例的 Class
属性。(更多的信息请参考: Instantiation using an instance factory method and Bean definition inheritance),有两种使用 Class
属性的方式:
- 典型的,指定需要实例化的
bean
的class
,容器通过反射调用bean
的构造方法实例化bean
,这种方式等价于java
中使用new
操作符。 - 指定包含创建对象的
static factory method
的类,容器调用类上面的静态工厂方法创建bean
,静态工厂方法返回当前类型对象或者其他类的对象,这种方式不常用。
内部类名称
如果需要配置一个静态的内部类,需要使用内部类的另外一个名称。
例如:如果在com.example
包中有一个Foo
类,这个类包含一个静态的内部了Bar
,静态内部类的bean
定义中元素的
class
属性应该是:com.example.Foo$Bar
,内部类和外部类需要用$
分割。
构造方法实例化bean
使用构造方法的方式创建 bean
时,正常的 class
都能使用,也就是说,这个类不需要实现特定的接口,也不要按指定的方式去编写,简单的指定 bean
的 class
就足够了。然而,你也许需要指定一个默认的构造方法,这取决于使用的容器。
Spring IoC Container
几乎能管理所有你想要管理的类,不限于真正的 JavaBeans
,大多数用户喜欢使用只有一个默认构造方法以及操作属性的 getter
/setter
方法的 JavaBeans
,如果需要,也可以在容器中使用不是 bean
风格的类。例如:Spring
可以管理一个不符合 JavaBean
规范的遗留连接池。
基于 XML 的配置,可以使用下面的方式指定 bean
的 class
:
如果想了解给构造方法传递参数(如果需要)以及给构造的对象设置属性的原理,可以参考:Injecting Dependencies.
静态工厂方法实例化bean
当需要静态工厂方法定义 bean
时,需要将 class
属性指定为包含静态工厂方法的类,将 factory-method
指定为静态工厂方法,你可以调用这个方法(可能带参数)并且返回一个对象,随后这个对象可以当做是通过构造方法创建的。静态工厂方法可以用在一些遗留的代码中。
以下的 bean
定义通过调用 factory-method
方法创建 bean
,定义中没有指定静态工厂方法返回的类型,只是指定了包含静态工厂方法的类,这个例子中,createInstance()
是一个静态方法。
public class ClientService{
private static ClientService clientService = new ClientService();
private ClientService(){
}
public static ClientService createInstance(){
return clientService;
}
}
关于给静态工程方法传递参数,工厂方法返回对象后给对象实例属性赋值的详细说明,请参考:Dependencies and configuration in detail.
实例工厂方法实例化bean
类似于上节中的静态工厂方法,实例工厂方法是通过容器中已经存在的一个 bean
上面的实例方法创建一个 bean
,要使用这种方式创建 bean
,不需要设置 class
,factory-bean
属性设置为在当前容器(或者父容器)中包含工厂方法的 bean
,factory-method
属性设置为工厂方法的名称。
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
}
一个工厂类中可以有多个工厂方法:
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
private static AccountService accountService = new AccountServiceImpl();
public ClientService createClientServiceInstance() {
return clientService;
}
public AccountService createAccountServiceInstance() {
return accountService;
}
}
如果需要了解如何通过依赖注入配置和管理 factory bean
,请参考:Dependencies and configuration in detail.
在 Spring 文档中,
factory bean
指的是包含用于创建其他bean
的实例或静态工厂方法的bean
,相反,FactoryBean
指定是 Spring 中的一个接口。
总结
-
BeanDefinition
和bean
的关系 - 如何配置
bean
,bean
有哪些属性 - 给
bean
命名 - 如何创建
bean
1.4 依赖
一个典型的企业级应用不会由一个对象(也就是Spring 中的 bean
)组成,即使很简单的应用,也需要好几个对象相互协作展现给终端用户。下一节中会解释如何通过单独定义的多个 bean
相互协作实现这个目标。
依赖注入(DI)是一个对象定义依赖关系的过程,也就是对象只能通过构造方法参数,工厂方法参数,或者(对象通过new或者工厂方法返回以后)设置属性来定义与其他对象的依赖关系,然后容器在创建Bean时注入这些依赖项。这个过程本质是反转的,因此得名控制反转,其含义是bean自己通过类的构造函数控制对象的初始化或者依赖发现,还是通过像 Service Locator Pattern机制。(关于什么服务定位模式,可以阅读这篇博客Service Locator Pattern)。
遵循 DI
原则可以让代码更简洁,当对象提供依赖关系时,解耦更彻底。对象不需要查找它的依赖,也不需要知道依赖的位置和类。这样,类变的更容易测试,如果依赖的是接口或者抽象类,在单元测试中可以通过打桩或者Mock来实现。
DI
存在两种主要的变体,分别是:Constructor-based dependency injection and Setter-based dependency injection.
构造方法参数注入
容器通过调用类的构造方法,传递参数,每个参数代表一个依赖来实现基于构造方法参数的注入,给静态工厂方法传递参数来创建对象也是类似的。接下来的讨论对于参数传递给构造方法还是工厂方法是类似的。下面的例子展示了通过构造方法参数注入,这个类没有特别的设定,它是一个不需要依赖容器的特定接口、抽象类或者标签的POJO类。
今天工作比较忙,只能现在来更新(2018/10/31 20:44),感谢各位捧场~~。
public class SimpleMovieLister{
// the SimpleMovieLister has a dependency on a MovieFinder
private MovieFinder movieFinder;
// a constructor so that the Spring container can inject a MovieFinder
public SimpleMovieLister(MovieFinder movieFinder){
this.movieFinder = movieFinder;
}
}
构造方法参数解析
构造方法的参数是通过类型解析和匹配的。如果 bean
定义的构造方法参数没用潜在的歧义,容器会使用 bean
定义的构造方法参数的顺序使用合适的构造方法实例化对象。请考虑下面定义的类。
package x.y;
public class ThingOne{
public ThingOne(ThingTwo thingTwo, ThingThree thingThree){
//......
}
}
假如上面的类 ThingTwo
和 ThingThree
之间没有继承关系,就不存在潜在的歧义。因此,不需要显示的在
元素中指定 type
和 index
属性,下面的配置就可以正常工作。
当引用另一个bean时,类型是已知的,并且可以进行匹配(就像前面的例子那样)。当使用简单类型(如
package examples;
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private int years;
// The Answer to Life, the Universe, and Everything
private String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
构造参数类型匹配
在上面的场景中,如果通过 type
属性显示指定了构造参数的类型,容器就可以对简单类型进行类型匹配。请看下面的例子:
构造参数索引:
可以通过 index
属性显示的指定构造参数的索引,例如:
显示的指定构造参数的索引除了可以解决多个简单类型值得歧义之外,还可以解决多个类型相同的构造参数。
index
是从0开始。
构造参数名称:
也可以使用构造参数名称解决歧义,比如:
请记住,要使这一特性开箱即用,你的代码必须在启用调试标志的情况下进行编译,以便 Spring 可以从构造函数查找参数名。如果没有使用调试标志编译代码,也可以使用 @ConstructorProperties
JDK 注释显式地为构造函数参数命名。例如:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
基于 setter 的依赖注入:
容器调用无参的构造方法或者静态工厂方法创建对象,然后调用 setter 方法实现依赖注入。
下面的例子展示了只通过 setter 方法依赖注入,这是一个普通的 java 类,不需要依赖容器特定的接口、抽象类或者注解(个人习惯叫标签,为了统一,后面的篇幅中还是使用注解)。
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on the MovieFinder
private MovieFinder movieFinder;
// a setter method so that the Spring container can inject a MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
ApplicationContext
不仅支持 bean
通过 setter 方法注入或者构造方法注入,也支持这两种方式混合注入。你可以用 BeanDefinition
的形式配置依赖关系,并将其与 PropertyEditor
实例一起使用,以将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户并不直接使用这些类(即通过编程方式),而是使用XML bean
定义、带注解的组件(@Component
、@Controller
等注释的类)或基于java的 @Configuration
类中的 @Bean
方法。然后,这些源在内部转换为 BeanDefinition
实例,并用于加载整个Spring IoC容器实例。
构造参数注入和 setter 方法注入比较
由于可以混合使用基于构造方法参数和基于setter 方法的DI,因此使用构造方法处理强制依赖关系,setter方法或配置方法(?)处理可选依赖关系是一个不错的经验。但是请注意,在setter方法上使用@Required
注释可以使属性成为必需的依赖项。
Spring 团队通常支持构造方法注入,因为它允许你将应用程序组件实现为不可变对象,并确保所需的依赖项不为空。此外,构造方法注入的组件总是以完全初始化的状态返回给客户端(调用)代码。作为补充说明,大量的构造方法参数是一种糟糕的代码风格,这意味着类可能有太多的职责,为了更好地处理关注点的适当分离,应该重构。
Setter注入主要应用于可选的依赖项,这些依赖项可以在类中设置合适的默认值。否则,在代码使用依赖项的任何地方都必须执行非空检查。setter注入的一个好处是,setter方法使该类的对象可以稍后重新配置或重新注入。因此,通过JMX mbean进行管理是setter注入的一个不错的用例。
对特定类使用最有意义的DI风格。有时候,在处理没有源代码的第三方类时,会为你做出选择。例如,如果第三方类不公开任何 setter 方法,那么构造函数注入可能是惟一可用的 DI 形式。
依赖解析过程说明:
容器是通过以下几个步骤完成 bean
的依赖解析。
-
ApplicationContext
是通过所有bean
的配置元数据创建和初始化的。配置元数据可以通过 XML 、Java Annoations以及Java Code 指定。 - 对于每个
bean
,它们的依赖项是通过 属性(setter)、构造方法参数或者静态构造方法参数表示。当对象真正创建以后,这些依赖会注入到对象中。 - 每个属性或者构造函数参数都是要设置的值的定义,或者容器中其他
bean
的引用。 - 每个属性或者构造函数的参数的值的格式(类型)会被转化为属性或者构造函数参数的真实类型。默认情况下,Spring 可以将字符串格式的值转化为任何内置类型,必须
int
,long
,String
,boolean
等等。
Spring 容器在创建的时候会校验每个 bean
的配置,然而, bean
在只有创建以后才会设置自己的属性。 如果 bean
的 scope
是 singleton-scope
并且设置了提前实例化(默认)时,则容器创建以后就会创建 bean
,否则,只有在需要的时候才会被创建。创建一个 bean
时可能因为依赖关系会创建出一系列其他 bean
。找不到依赖的 bean
的情况只会在正真创建 bean
的时候发生。
循环依赖
如果使用构造函数注入,可能会遇到循环依赖的问题。
例如:类 A 使用构造函数注入类 B,类 B 使用构造函数注入类 A ,这种情况下容器在运行时会检测到循环依赖,并且抛出一个BeanCurrentlyInCreationException
的异常。
一种可行的方案是使用 setter 方法注入而不是构造函数注入,虽然这种方法是不推荐的,但是确实可以解决循环依赖的问题。
与一般的情况(没有循环依赖)不同,bean A和bean B之间的循环依赖关系迫使其中一个bean
在被 完全初始化之前 注入另一个bean
(典型的鸡生蛋,蛋生鸡场景)。
通常,可以相信 Spring 会正确的事情,在容器加载期间,它会检测到诸如依赖的 bean
不存在或者循环依赖等配置问题。当创建真实的对象时,Spring 会尽可能晚的设置属性和解析依赖。这意味着 Spring 容器正常加载以后,只有在正真创建对象或者对象的依赖时,可能抛出异常,而不是容器加载以后就抛出异常,这样都导致一些配置问题不能马上暴露,这也是为什么 ApplicationContext
实现默认实例化单例 bean
。在 bean
需要使用之前,花费一些时间和内存提前创建,这样一些问题就会在容器创建的时候发现,而不是之后。也可以重写默认的行为,让单例 bean
延迟实例化而不是预实例化。
如果不存在循环依赖,当一个或多个协作者被注入到一个 bean
时,每个协作者在注入之前需要完全被配置。这也就是如果 bean A 需要依赖一个 bean B,在调用 bean A 的 setter 方法之前,需要优先配置好 bean B,
换句话说,bean
被实例化(非预实例化bean) ,它的依赖被设置,然后才是调用相关的生命周期函数(比如:configured init method or the InitializingBean callback method)
依赖注入示例
下面的例子展示了基于 XML 的 setter 注入配置,有一小部分配置文件指定了 bean 的定义。
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}
public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}
public void setIntegerProperty(int i) {
this.i = i;
}
}
上面的例子中属性是通过 setter 方法注入设置的。下面的例子使用构造方法注入:
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
public ExampleBean(
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
this.beanOne = anotherBean;
this.beanTwo = yetAnotherBean;
this.i = i;
}
}
在 bean
定义中指定的构造参数将被传递给了 ExampleBean
的构造函数。
现在考虑一个衍生示例,用静态构造方法代替构造函数创建对象:
public class ExampleBean {
// a private constructor
private ExampleBean(...) {
...
}
// a static factory method; the arguments to this method can be
// considered the dependencies of the bean that is returned,
// regardless of how those arguments are actually used.
public static ExampleBean createInstance (
AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
// some other operations...
return eb;
}
}
静态工厂方法的参数也是通过
标签提供,和构造方法一样。静态工厂方法返回的类型不一定和拥有它的类一样,虽然这里是一样的。实例工厂方法也是一样方式,只不过将 class
属性替换为包含实例方法的 bean
,详请就不在这里讨论了。
1.4.2 依赖和配置详细说明
正如上节中提到的,可以通过定义 bean
的属性或构造参数来引用其他的 bean
或者简单值。基于 XML 的配置可以通过元素
和
实现这个目的。
简单值(基本类型、Strings等等)
元素的 value
属性通过一个可读的字符串指定属性或者构造参数。利用 Spring 的 conversion service 将这些 String
值转化为属性或构造参数所需类型值。下面的例子展示设置各种值:
下面的例子使用 p-namespace 简化 XML 配置。
上面的 XML 更加简洁,但是,拼写错误只能在运行时发现而不是编辑时,除非你使用 IDE(比如: IntelliJ IDEA 或者 Spring Tool Suite)。
你也可以使用下面的方式配置一个 java.util.Properties
实例:
jdbc.driver.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
Spring 容器使用 PropertyEditor
机制将
元素中的文本转化为 java.util.Properties
实例。这是一个完美的快捷方式,这也是 Spring 团队喜欢使用
元素内嵌文本风格而不是 value
属性风格的少有的地方之一。
idref 元素
元素 idref
通过 bean
属性(是一个字符串,而不是引用)来验证
或者
元素中引用的其他 bean
是否存在(请记住,idref只是验证bean是否存在,并不是引用bean)。下面展示了如何使用。
上面的配置中,targetName
在类中是一个字符串字段,而不是 theTargetBean
类型的对象引用。
它与下面的配置等价:
第一种配置形式比第二种好,因为,idref
标签可以使容器在部署阶段端验证指定名称的 bean
是否存在。第二种方式没有验证传递给 targetName
属性的 bean
(是否存在),对于拼写错误只能到 bean
真正实例化时才能发现,如果 bean
的 scope
是 prototype
,这个错误可能会在容器部署完很长一段时间之后才能发现。
标签idref上的local属性,在4.0版本中的xsd就已经不在支持了,因为它不能再为bean提供一个引用值了。当升级至4.0版本时,只要将你项目中的idref local替换为idref bean即可。
一个通用的方式(至少是在 Spring 2.0之前),
元素携带值在 ProxyFactoryBean
定义中配置 AOP interceptors ,使用
可以防止拼错拦截器的ID。
引用其他bean(协作者)
ref
元素是一个嵌在
或
元素中的终子元素(没有子元素的元素),可以通过设置 bean
属性来引用容器中的其他 bean
,被引用的 bean
是这个 bean
的依赖,协作(被引用)bean
需要在属性设置之前实例化。(如果协作 bean是一个单例 bean,也许已经被容器实例化)。基本上所有的 bean
都有一个引用的 bean
,作用域和校验依赖于你是否通过 bean
、local
、parent
属性指定了其他对象的 ID
或者 name
。
通过 元素的
bean
属性来创建相同容器或者父容器中的其他 bean
的引用是一种常用的方式,不管他们是否在同一个 XML 文件里。bean
属性可能是协作 bean
的 ID
或者 name
值。下面展示如何使用 ref
元素:
通过 ref
元素的 parent
属性创建一个引用父容器的 bean
,parent
属性的值是协作 bean
的 ID
或者其中一个 name
值,协作 bean
必须在父容器中存在。这种引用指定的方式主要使用在容器继承中,如果子容器想要创建一个与父容器 的 bean
具有相同名称的代理。下面的例子展示了如何使用 parent
属性:
class="org.springframework.aop.framework.ProxyFactoryBean">
标签
local
上的local属性,在4.0版本中的xsd就已经不在支持了,因为它不能再为bean提供一个引用值了。当升级至4.0版本时,只要将你项目中的ref local替换为ref bean即可。
内部 bean
元素
或
的子元素
可以定义内部 bean
,如下所示:
内部 bean
不需要指定 ID
或 name
,即便是指定了,容器也不会把它当做 bean
的标识符。容器在创建时也会忽略 scope
标志,因为内部 bean
常常是匿名的并且是伴随外部 bean
创建的。内部 bean
是不可能被单独访问,或者注入到除了外部 bean
的其他 bean
中。
一个特例,内部 bean
可以接受自定义 scope
的销毁回调,举个例子,在一个 singleton
bean
中包含一个 request-scoped
内部 bean
,虽然内部 bean
的创建和包含它的外部 bean
绑定到一起,但是销毁回调允许它参与 request scope
的生命周期。
集合
元素
、
、 和
分别设置 Java 集合类型 List
、Set
、Map
和 Properties
的属性或方法参数。下面展示如何使用它们:
[email protected]
[email protected]
[email protected]
just some string
Map
的键值或者集合的值可以通过以下元素设置:
bean | idref | ref | null | value | list | set | map | props
集合合并
Spring 容器也支持合并集合。应用开发人员可以定义继承自父元素
、
、 或者
的子元素
、
、 或者
来覆盖父集合的值。也就是说,子集合的值是父集合和子集合合并后的值。
这节会涉及到父子 bean
合并机制,如果读者不熟悉父子 bean
的定义,在继续下面的内容之前请先阅读 relevant section。
下面的例子展示集合合并:
[email protected]
[email protected]
[email protected]
[email protected]
请注意在定义子 bean
的 adminEmails
属性时需要在元素
上面使用 merge=true
属性。当容器解析和实例化 child
bean
的时候,实例有一个类型 Properties
集合的属性 adminEmails
的结果是子集合 adminEmails
和 父集合 adminEmails
合并后的结果,下面是合并后集合的值:
这个合并行为也可以应用于
、
和 类型的集合,
有一个特殊情况,它的元素的顺序在合并以后不会改变,父集合的值排在子集合的前面。对于 Map
、Set
和 Properties
类型的集合不存在排序的问题。也就是说,在容器内部,对于实现 Map
、Set
和 Properties
的类型没有排序的语义。
集合合并的限制
不能合并不同类型的集合(比如:Map
和 List
),如果试图这样做,会得到一个对应的 Exception
。merge
属性必须在 child bean
的定义中指定,如果在 parent bean
中指定了,那么你不会得到你想要的结果。
强类型集合
自从 Java 5 中引入了泛型类型之后,你可以使用强类型的集合。也就是说,可以定义一个只包含 String
类型元素的集合(举例)。如果你使用 Spring 来向 bean
中注入一个强类型的集合,Spring 会在元素添加到集合之前将定义的元素转化为合适的类型。下面展示如何使用:
public class SomeClass {
private Map accounts;
public void setAccounts(Map accounts) {
this.accounts = accounts;
}
}
当 something
bean
的属性 accounts
准备被注入时,强类型集合 Map
的元素类型的泛型信息会通过反射拿到,因此,Spring 的类型转化功能会把这些元素值认为是 Float
类型,字符串值(9.99,2.75 和 3.99)会被转化为 Float
类型。
Null 和 空字符串值
Spring 会将属性的空参数当做是空字符串处理。下面基于 XML 的配置片段将 email
属性的值设置为空字符串("")。
上面的配置和下面的 java 代码等价:
exampleBean.setEmail("");
元素用来处理 null
值。
上面的配置等价于下面的 java 代码:
exampleBean.setEamil(null);
p-namespace
c-namespace
p-namespace
允许你使用 bean
元素的属性(代替内嵌的
元素)来描述属性的值(简单值或引用)。
Spring 支持通过基于 XML 模式定义的 with namespaces 来扩展配置格式。在本章中 bean
的配置格式是在 XML 模式文档中定义,然而 p-namespace
不是在 XSD
文件中定义的,它只存在于 Spring 的核心。
下面展示了两段 XML 片段(第一段使用标准的 XML 格式,第二段使用 p-namespace
),它们解析后的结果是相同的。
上面的例子展示了在 p-namespace
中有一个叫 email
的属性,这就告诉 Spring 包含一个属性的声明。上面提到,p-namespace
不是一个模式定义,所以类字段的名称可以设置为属性的名称。
下面的例子展示了两个 bean
的定义,它们都有一个引用其他 bean
的属性。
这个例子不仅包含了使用 p-namespace
设置简单属性值,也包含了设置引用属性值。第一个 bean
定义使用
创建了一个从 bean
john
到 bean
jane
的引用,第二个 bean
定义使用 p:spouse-ref="jane"
做了同样的事情。在这种情况下,spouse
是属性的名称,而后缀 -ref
表示这不是一个简单值而是一个引用(这里的引用不包含 String)。
p-namespace并不如标准XML定义灵活。例如声明引用属性时与以Ref结尾的属性冲突。我们建议你慎重选择,并与团队成员做好沟通,避免同时使用全部三种方式(
的属性、 的子元素以及 的属性)。
c-namespace
类似于 XML Shortcut with the p-namespace,c-namespace
是在 Spring 3.1 引入,允许使用行内属性配置构造参数而不是内嵌元素
。
下面展示了使用 c:
命名空间做和 Constructor-based Dependency Injection 一样的事情:
c:
命名空间使用了和 p:
相同的规则(以 -ref
后缀表示引用)通过名称来定义构造参数。类似的,它可以声明参数名称即使不在 XSD
模式中定义(它存在于 Spring core 中)。
在少数情况下,构造参数名称不能获取(如果字节码文件是在没有调试信息的情况下编译的),你可以退而求次使用参数的索引,比如:
由于 XML 语法规定属性名不能以数字开口,所以索引值需要前缀
_
在实践中,构造函数解析机制在匹配参数方面是非常不错的(一般情况都可以处理),所以除非逼不得已才像上面这么做,我们建议在配置中使用名称表示法来贯穿整个配置。
组合属性值
当设置 bean
的属性时,你可以使用组合的,嵌套的属性名称,只要除了最终需要设置的属性,其他每一级的属性都不为null。请看下面的配置:
something
bean
有一个 fred
的属性,fred
有一个 bob
属性,bob
有一个 sammy
属性,然后最终的属性 sammy
被设置为123。为了能正常工作, something
bean
被构建后,属性 fred
,fred
的属性bob
不能为null,否则,就会抛出NullPointerException(空指针异常)。
1.4.3. 使用 depends-on
如果一个 bean
是另外一个 bean
的依赖,通常是这个 bean
是作为另外一个 bean
的属性。典型情况下,在基于 XML 的配置中是通过 元素实现的。然而,有些情况下,两个
bean
之间是间接关系。比如:当类中的一个静态初始化方法需要被触发,比如注册数据库驱动,depends-on
属性可以强制在这个 bean
实例化之前先实例化指定的 bean
。下面的例子使用 depends-on
属性表示对一个 bean
的依赖:
为了表达依赖多个 bean
,可以给 depends-on
属性指定多个 bean
名称的列表(逗号、分号或者空格分割)。
bean定义中的depends-on属性,可以指定初始化时候的依赖,在单例bean中也可以指定销毁时间的依赖。在此bean本身被销毁之前,被指定的依赖bean首先被销毁,因此,depends-on也可以控制关闭的顺序。
1.4.4 懒加载 bean
默认,ApplicationContext
的实现会尽早的创建和配置所有的单例 bean
,并将此作为初始化过程的一部分。通常,预实例化是你需要的,因为配置错误或环境错误会被立即发现,而不是几个小时或几天之后。当这个默认行为不是你想要的,可以在
元素上指定 lazy-init
属性来阻止单例 bean
预实例化。懒加载 bean
告诉容器只有在它被需要的时候再实例化,而不是启动的时候。
在 XML 中,这个行为可以通过
元素的 lazy-init
属性控制,示例如下:
当上面的配置被一个 ApplicationContext
使用时, ApplicationContext
启动时,lazy bean
不会被尽早的实例化,而 not.lazy bean
会被尽早的预实例化。
然而,如果一个懒加载的 bean
是一个非懒加载的 bean
的依赖,ApplicationContext
会在启动的时候创建懒加载的 bean
,因为它必须满足单例的依赖。懒加载的 bean
被注入到单例 bean
中,它在其他地方也就成了非懒加载的 bean
了。
也可以通过
元素的 default-lazy-init
属性,在容器级控制懒加载。例如: