1. IoC概念简介
IoC是随着近年来轻量级容器(Lightweight Container)的兴起而逐渐被很多人提起的一个名词,它的全称为Inversion of Control,中文通常翻译为“控制反转”。好莱坞原则“Don’t call us, we will call you.”恰如其分地表达了“反转”的意味,是用来形容IoC最多的一句话。
它不是什么技术,而是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由Spring框架来管理。
- 正控:若要使用某个对象,需要自己去负责对象的创建
- 反控:若要使用某个对象,只需要从Spring容器中获取需要使用的对象,不关心对象的创建过程,也就是把创建对象的控制权反转给了Spring框架
2. 依赖注入
依赖注入(Dependency Injection,简称DI),2004年,Martin Fowler探讨了一个问题,既然IOC是控制反转,那么到底是“哪些方面的控制被反转了呢?”,经过详细地分析和论证后,他得出了答案:“获得依赖对象的过程被反转了”。控制被反转之后,获得依赖对象的过程由自身管理变为了由IoC容器主动注入。
于是,他给“控制反转”取了一个更合适的名字叫做“依赖注入(Dependency Injection)”。他的这个答案,实际上给出了实现IoC的方法:注入——所谓依赖注入,就是由IoC容器在运行期间,动态地将某种依赖关系注入到对象之中。
所以,依赖注入(DI)和控制反转(IoC)是从不同的角度的描述的同一件事情,就是指通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦。
或者说,IoC是一种思想,是一种目标,而DI这时一种手段,一种过程。
2.1 理论上的依赖注入方式
在学术理论上,依赖注入有三种实现方式:
2.1.1 三种注入的方式
当你来到酒吧,想要喝杯啤酒的时候,通常会直接招呼服务生,让他为你送来一杯清凉的啤酒。同样地,作为被注入对象,要想让IoC容器为其提供服务,并将所需要的被依赖对象送过来,也需要通过某种方式通知对方。
这里就牵涉到了三种依赖注入的方式:
- 构造方法注入
- 顾名思义,构造方法注入,就是被注入对象可以通过在其构造方法中声明依赖对象的参数列表,让外部(通常是IoC容器)知道它需要哪些依赖对象。
1
2
3
4public FXNewsProvider(IFXNewsListener newsListner,IFXNewsPersister newsPersister) {
this.newsListener = newsListner;
this.newPersistener = newsPersister;
} - IoC Service Provider会检查被注入对象的构造方法,取得它所需要的依赖对象列表,进而为其注入相应的对象。同一个对象是不可能被构造两次的,因此,被注入对象的构造乃至其整个生命周期,应该是由IoC Service Provider来管理的。构造方法注入方式比较直观,对象被构造完成后,即进入就绪状态,可以马上使用。
这就好比你刚进酒吧的门,服务生已经将你喜欢的啤酒摆上了桌面一样。坐下就可马上享受一份清凉与惬意。
- 顾名思义,构造方法注入,就是被注入对象可以通过在其构造方法中声明依赖对象的参数列表,让外部(通常是IoC容器)知道它需要哪些依赖对象。
- setter 方法注入
- 对于JavaBean对象来说,通常会通过setXXX()和getXXX()方法来访问对应属性。所以,当前对象只要为其依赖对象所对应的属性添加setter方法,就可以通过setter方法将相应的依赖对象设置到被注入对象中。
1
2
3
4
5
6
7
8
9
10public class FXNewsProvider{
private IFXNewsListener newsListener;
public IFXNewsListener getNewsListener() {
return newsListener;
}
public void setNewsListener(IFXNewsListener newsListener) {
this.newsListener = newsListener;
}
} - 这样,外界就可以通过调用setNewsListener方法为FXNewsProvider对象注入所依赖的对象了。setter方法注入虽不像构造方法注入那样,让对象构造完成后即可使用,但相对来说更宽松一些,可以在对象构造完成后再注入。
这就好比你可以到酒吧坐下后再决定要点什么啤酒,可以要百威,也可以要青岛,随意性比较强。如果你不急着喝,这种方式当然是最适合你的。
- 对于JavaBean对象来说,通常会通过setXXX()和getXXX()方法来访问对应属性。所以,当前对象只要为其依赖对象所对应的属性添加setter方法,就可以通过setter方法将相应的依赖对象设置到被注入对象中。
- 接口注入
- 首先注意,因为代码侵入性高,所以这种方式Spring框架不支持,只要了解即可。
- 相对于前两种注入方式来说,接口注入没有那么简单明了。被注入对象如果想要IoC Service Provider为其注入依赖对象,就必须实现某个接口。这个接口提供一个方法,用来为其注入依赖对象。IoC Service Provider最终通过这些接口来了解应该为被注入对象注入什么依赖对象。
- FXNewsProvider为了让IoC Service Provider为其注入所依赖的IFXNewsListener,首先需要实现IFXNewsListenerCallable接口,这个接口会声明一个injectNewsListner方法(方法名随意),该方法的参数,就是所依赖对象的类型。这样,InjectionServiceContainer对象,即对应的IoC Service Provider就可以通过这个接口方法将依赖对象注入到被注入对象FXNewsProvider当中。
- 接口注入方式最早并且使用最多的是在一个叫做Avalon的项目中,相对于前两种依赖注入方式,接口注入比较死板和烦琐。如果需要注入依赖对象,被注入对象就必须声明和实现另外的接口。
这就好像你同样在酒吧点啤酒,为了让服务生理解你的意思,你就必须戴上一顶啤酒杯式的帽子
2.1.2 三种注入方式的比较
- 接口注入:
- 从注入方式的使用上来说,接口注入是现在不甚提倡的一种方式,基本处于“退役状态”。因为它强制被注入对象实现不必要的接口,带有侵入性。而构造方法注入和setter方法注入则不需要如此。
- 构造方法注入:
- 这种注入方式的优点就是,对象在构造完成之后,即已进入就绪状态,可以马上使用。
- 缺点就是,当依赖对象比较多的时候,构造方法的参数列表会比较长。而通过反射构造对象的时候,对相同类型的参数的处理会比较困难,维护和使用上也比较麻烦。而且在Java中,构造方法无法被继承,无法设置默认值。对于非必须的依赖处理,可能需要引入多个构造方法,而参数数量的变动可能造成维护上的不便。
- setter方法注入:
- 因为方法可以命名,所以setter方法注入在描述性上要比构造方法注入好一些。另外,setter方法可以被继承,允许设置默认值,而且有良好的IDE支持。
- 缺点当然就是对象无法在构造完成后马上进入就绪状态。
综上所述,构造方法注入和setter方法注入因为其侵入性较弱,且易于理解和使用,所以是现在使用最多的注入方式;而接口注入因为侵入性较强,近年来已经不流行了。
2.2 Spring的依赖注入方式
因为代码侵入性高的问题,接口注入的方式,spring框架并不支持。Spring的依赖注入方式只有构造方法注入和setter方法注入:
- 构造方法
- 开箱即用,适合用于注入实例必须的初始值时使用,但是当参数列表较长时难以维护和使用。构造方法无法被继承,也无法设置默认值。适合较固定的对象使用。
1
2
3
4
5<bean id="login" class="com.spring.test.di.LoginImpl"/>
<bean id="loginAction" class="com.spring.test.di.LoginAction">
<constructorarg index="0" ref="login"></constructorarg>
</bean>
- 开箱即用,适合用于注入实例必须的初始值时使用,但是当参数列表较长时难以维护和使用。构造方法无法被继承,也无法设置默认值。适合较固定的对象使用。
- setter方法
- 适合依赖对象多,且组成对象灵活多变的场景,是目前最为常见的注入方法。
1
2
3
4
5<bean id="login" class="com.spring.test.di.LoginImpl"/>
<bean id="loginAction" class="com.spring.test.di.LoginAction">
<property name="login" ref="login"></property>
</bean>
- 适合依赖对象多,且组成对象灵活多变的场景,是目前最为常见的注入方法。
3. IoC Service Provider
了解了IoC和DI的概念中,我们可以知道,在DI的过程中,IoC Service Provider是一个非常重要的概念——业务对象可以通过IoC方式声明相应的依赖,但是最终仍然需要通过某种角色或者服务将这些相互依赖的对象绑定到一起,IoC Service Provider就是这样一个角色。
IoC Service Provider在这里是一个抽象出来的概念,它可以指代任何将IoC场景中的业务对象绑定到一起的实现方式。它可以是一段代码,也可以是一组相关的类,甚至可以是比较通用的IoC框架或者IoC容器实现。Spring的IoC容器就是一个提供依赖注入服务的IoC Service Provider。
3.1 IoC Service Provider的职责
IoC Service Provider的职责相对来说比较简单,主要有两个:
- 业务对象的注册管理。
- 在IoC场景中,业务对象无需关心所依赖的对象如何构建如何取得,但这部分工作始终需要有人来做。所以,IoC Service Provider需要识别这部分需要管理的对象,并且将这些对象的构建逻辑从客户端对象那里剥离出来,以免这部分逻辑污染业务对象的实现。
- 业务对象间的依赖绑定。
- IoC Service Provider通过结合之前构建和管理的所有业务对象,以及各个业务对象间可以识别的依赖关系,将这些对象所依赖的对象注入绑定,从而保证每个业务对象在使用的时候,可以处于就绪状态。
3.2 常见IoC Service Provider依赖注册方式
那么,对于IoC Service Provider来说,如何知道哪些对象是被其他对象依赖(即需要它管理起来的)的呢?又是如何知道某个管理的对象,具体要注入到哪一个具体的其他对象中呢?就像一个资深的酒吧服务员,客人点了哪些酒,且每一杯酒分别是被哪个客人点的,他都要了然于心,这时如何做到的呢?
很显然,我们需要记录下来这些的“服务信息”(在Spring的术语中,把BeanFactory需要使用的对象注册和依赖绑定信息称为Configuration Metadata),当前流行的IoC Service Provider产品使用的Configuration Metadata的方式主要有以下几种:
- 直接编码方式
- 当前大部分的IoC容器都应该支持直接编码方式,比如PicoContainer、Spring、Avalon等。在容器启动之前,我们就可以通过程序编码的方式将被注入对象和依赖对象注册到容器中,并明确它们相互之间的依赖注入关系。
- 配置文件方式
- 这是一种较为普遍的依赖注入关系管理方式。像普通文本文件、properties文件、XML文件等,都可以成为管理依赖注入关系的载体。不过,最为常见的,还是通过XML文件来管理对象注册和对象间依赖关系,比如Spring IoC容器和在PicoContainer基础上扩展的NanoContainer,都是采用XML文件来管理和保存依赖注入信息的。
- 元数据方式(注解)
- 这种方式的代表实现是Google Guice,这是Bob Lee在Java 5的注解和Generic的基础上开发的一套IoC框架。我们可以直接在类中使用元数据信息来标注各个对象之间的依赖关系,然后由Guice框架根据这些注解所提供的信息将这些对象组装后,交给客户端对象使用。
3.3 Spring IoC Service Provider依赖注册方式
Spring IoC Service Provider的注册依赖方式同样是三种,也就是说,其他IoC Service Provider支持的主流的三种依赖注册方式,Spring都支持。
- 直接编码方式
- 使用
@Configuration
注解可以将java的类文件声明成spring的配置类,使用@Bean
来声明方法的返回对象要注册为spring的bean对象。 - 而当bean中需要注入其他参数或者引用时,将其作为方法的参数即可,Spring会帮你注入这些引用。
- 默认情况下,方法名即为id名,当然也可以为bean指定名称,通过其
@Bean
注解的name属性。 - 同时
@Bean
注解的initMethod属性和destroyMethod属性,可以指定初始化和销毁时的生命周期回调函数。 - 而
@Scope
和@Description
注解,则可以给bean设置Scope和Description1
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
35
36
37
38//使用@Configuration注解可以将java的类文件声明成spring的配置类
@Configuration
public class SpringConfig {
@Bean //你可以理解为定义一个String类型的bean,值是"test",做依赖注入用。
public String username(){
return "test";
}
@Bean
public List<String> tags(){
List<String> tags = new ArrayList<String>();
tags.add("cool");
tags.add("nice");
return tags;
}
//使用@Bean 注解表明myBean需要交给Spring进行管理
//如果未指定bean的id,默认采用的是 "方法名" + "首字母小写"的配置方式
//name属性可以定义bean的id ; initMethod和destroyMethod属性指定初始化和销毁时的生命周期回调函数。
@Bean(name = "userInterfaceIml" , initMethod = "init" , destroyMethod = "cleanup")
@Scope("prototype") //指定该bean的scope
@Description("Provides a basic example of a bean") //指定该bean的description
public UserInterface userInterface(){
return new UserInterfaceImpl();
}
@Bean
//通过参数列表,将bean的依赖注入
public UserCall userCall(UserInterface userInterface, String username, List<String> tags){
UserCall uc = new UserCall();
uc.setUi(userInterface);
uc.setUsername(username);
uc.setTags(tags);
return uc;
}
}
}
- 使用
- 配置文件方式
- Spring使用XML文件来管理和保存依赖注入信息,配置组件bean的话只需要使用
<bean>
标签即可。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<bean id="userInterface" class="com.springbean.impl.UserInterfaceImpl" />
//使用构造器注入。使用构造器注入的时候必须在类中存在对应的构造方法才能有效
<util:list id="tagsList">
<value>cool</value>
<value>nice</value>
</util:list>
<bean id="userCall" class="com.springbean.UserCall">
<constructor-arg name="ui" ref="userInterface"/>
<constructor-arg name="username" value="test"/>
<constructor-arg name="tags" ref="tagsList"></constructor-arg>
</bean>
//使用属性注入
<bean id="userCall" class="com.springbean.UserCall">
<property name="ui" ref="userInterface" />
<property name="username" value="test" />
<property name="tags" value="tagsList" />
</bean>
- Spring使用XML文件来管理和保存依赖注入信息,配置组件bean的话只需要使用
- 元数据方式(注解)
- spring支持通过注解方式管理依赖,但是需要指定spring扫描注解的包,指定扫描的包有两种方式
- 可以在Spring的xml文件中配置(前提是引入了Spring context的命名空间),使用
<context:component-scan base-package="com.springbean.*"/>
- 注解@ComponentScan指定了spring将扫描这个配置类所在的包及其子包下面的所有类。
1
2@ComponentScan
public class SpringConfig {}
- 可以在Spring的xml文件中配置(前提是引入了Spring context的命名空间),使用
- 有了组件扫描后,所有被注解@Component或者它衍生的注解标记类都将被识别为组件类,他们完善了spring通过注解来注册依赖的功能:
- @Component: 自动被comonent扫描。 表示被注解的类会自动被component扫描
- @Repository: 用于持久层,主要是数据库存储库。
- @Service: 表示被注解的类是位于业务层的业务component。
- @Controller:表明被注解的类是控制component,主要用于展现层 。
- 除此之外,spring使用注解
@Autowired
等完成依赖装配:- @Autowired:支持按类型自动转配
- @Qualifier:根据byName的方式自动装配,其中@Qualifier不能单独使用。
1
2
3
4
5public class User {
@Autowired
@Qualifier(value="carXXX")
private Cat cat;
} - @Resource(这个注解属于J2EE的):如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常;如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常;如果指定了type,则从上下文中找到类型匹配的唯一bean进行装配,找不到或者找到多个,都会抛出异常;如果既没有指定name,又没有指定type,则自动按照byName方式(字段名)进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配;
1
2
3
4
5
6public class User {
@Resource
private Cat cat;
@Resource(name="dogXXX")
private Dog dog;
}
- spring支持通过注解方式管理依赖,但是需要指定spring扫描注解的包,指定扫描的包有两种方式
4. Spring的IoC容器
上文中,我们从浅到深,从思想到概念,了解了DI过程中的一个重要的角色——IoC Service Provider。
IoC Service Provider只是一个概念,不同的框架,对IoC Service Provider的具体的实现也是五花八门,接下来我们了解一个完成度高,重要性高且知名度极高的IoC Service Provider实现产品——Spring IoC容器。
Spring的IoC容器是一个IoC Service Provider,但不止是一个IoC Service Provider,作为轻量级容器,Spring的IoC容器还提供了IoC之外的支持。如在Spring的IoC容器之上,Spring还提供了相应的AOP框架支持、企业级服务集成等服务。Spring的IoC容器和IoC Service Provider所提供的服务之间存在一定的交集,二者的关系如图
4.1 Spring IoC容器类型
Spring提供了两种容器类型:BeanFactory和ApplicationContext。
- BeanFactory。
- 基础类型IoC容器,提供完整的IoC服务支持。如果没有特殊指定,默认采用延迟初始化策略(lazy-load)。只有当客户端对象需要访问容器中的某个受管对象的时候,才对该受管对象进行初始化以及依赖注入操作。所以,相对来说,容器启动初期速度较快,所需要的资源有限。对于资源有限,并且功能要求不是很严格的场景,BeanFactory是比较合适的IoC容器选择。
- ApplicationContext。
- ApplicationContext在BeanFactory的基础上构建,是相对比较高级的容器实现,除了拥有BeanFactory的所有支持,ApplicationContext还提供了其他高级特性,比如事件发布、国际化信息支持等,这些会在后面详述。ApplicationContext所管理的对象,在该类型容器启动之后,默认全部初始化并绑定完成。所以,相对于BeanFactory来说,ApplicationContext要求更多的系统资源,同时,因为在启动时就完成所有初始化,容器启动时间较之BeanFactory也会长一些。在那些系统资源充足,并且要求更多功能的场景中,ApplicationContext类型的容器是比较合适的选择。
ApplicationContext包含BeanFactory的所有功能,几乎所有的应用系统都选择ApplicationContext而不是BeanFactory。只有在资源很少的情况下,才会考虑采用BeanFactory,如在移动设备上等。
通过下图,我们可以对BeanFactory和ApplicationContext之间的关系有一个更清晰的认识:
4.1.1 BeanFactory
在没有特殊指明的情况下,以BeanFactory为中心所讲述的内容同样适用于ApplicationContext,这一点需要明确一下,二者有差别的地方会在合适的位置给出解释。
BeanFactory,顾名思义,就是生产Bean的工厂。BeanFactory就像一个汽车生产厂。你从其他汽车零件厂商或者自己的零件生产部门取得汽车零件送入这个汽车生产厂,最后,只需要从生产线的终点取得成品汽车就可以了。至于业务对象如何组装,你不需要关心。
BeanFactory只是个interface,它核心实现,在DefaultListableBeanFactory实现类中。BeanFactory声明了如下的方法:
通过方法名我们也能大概了解每个方法的作用,基本上都是查询相关的方法,例如,取得某个对象的方法(getBean)、查询
某个对象是否存在于容器中的方法(containsBean),或者取得某个bean的状态或者类型的方法等。
这些api使得我们可以非常方便的从容器中获取特定类型的bean。那么,BeanFactory如何知道它需要管理和生成哪些bean呢?被托管的bean又是如何注册的呢?后文我们会就bean的注册/绑定/注入做深入介绍。
4.1.2 ApplicationContext
作为Spring提供的较之BeanFactory更为先进的IoC容器实现,ApplicationContext是BeanFactory的子类,故而ApplicationContext拥有BeanFactory支持的所有功能,但除此之外,还进一步扩展了基本容器的功能,如:更易与Spring AOP集成,容器启动后bean实例的自动初始化、国际化的信息支持、容器内事件发布等;
4.2 spring bean和bean定义
java bean对我们来说十分熟悉,我们把符合下面四点的java对象叫做java bean。
- 所有属性为private
- 提供默认构造方法
- 提供getter和setter
- 实现serializable接口
4.2.1 spring中的bean
spring中的bean是基于java bean概念的延伸,但为了更好的实现bean的注册/绑定/注入,spring bean的定义显然不能止步于此,为了更好的管理bean,spring在bean上做了许多拓展,不仅对bean本身的属性做纵向拓展,在横向的种类上,也按照不同的职责划分,定义了许多“专业”的,有特点功能的bean。
注意,特殊的bean也是基于普通bean的拓展,普通bean拥有的特点,特殊bean都有。
4.2.1.1 普通的spring bean
为了应对许多不同的场景,我们在配置spring bean的Configuration Metadata的时候,需要定义bean的许多属性来达到不同的目的,故而我们有必要了解spring为bean定义了哪些属性可用。
id属性
- 通常,每个注册到容器的对象都需要一个唯一标志来将其与“同处一室”的“兄弟们”区分开来,就好像我们每一个人都有一个身份证号一样(重号的话就比较麻烦)。通过id属性来指定当前注册对象的beanName是什么。
<bean id="djNewsListener" class="..impl.DowJonesNewsListener"> </bean>
name属性
- 除了可以使用id来指定
<bean>
在容器中的标志,还可以使用name属性来指定<bean>
的别名(alias) - 与id属性相比,name属性的灵活之处在于,name可以使用id不能使用的一些字符,比如/。而且还可以通过逗号、空格或者冒号分割指定多个name。
<bean id="djNewsListener" name="/news/djNewsListener,dowJonesNewsListener" class="..impl.DowJonesNewsListener"> </bean>
- 除了可以使用id来指定
class属性
- 每个注册到容器的对象都需要通过
<bean>
元素的class属性指定其类型,否则,容器可不知道这个对象到底是何方神圣。 - 在大部分情况下,该属性是必须的。仅在少数情况下不需要指定,如后面将提到的在使用抽象配置模板的情况下。
<bean id="djNewsListener" class="..impl.DowJonesNewsListener"> </bean>
- 每个注册到容器的对象都需要通过
scope属性
- scope用来声明容器中的对象所应该处的限定场景或者说该对象的存活时间,即容器在对象进入其相应的scope之前,生成并装配这些对象,在该对象不再处于这些scope的限定之后,容器通常会销毁这些对象。
- Spring容器最初提供了两种bean的scope类型:singleton和prototype,但发布2.0之后,又引入了另外三种scope类型,即request、session和global session类型。不过这三种类型有所限制,只能在Web应用中使用。
<bean id="mockObject2" class="...MockBusinessObject" scope="prototype"/>
- singleton:单例的意思。即标记为拥有singleton scope的对象定义,在Spring的IoC容器中只存在一个实例,所有对该对象的引用将共享这个实例。该实例从容器启动,并因为第一次被请求而初始化之后,将一直存活到容器退出。
- prototype:容器在接到该类型对象的请求的时候,会每次都重新生成一个新的对象实例给请求方。虽然这种类型的对象的实例化以及属性设置等工作都是由容器负责的,但是只要准备完毕,并且对象实例返回给请求方之后,容器就不再拥有当前返回对象的引用,请求方需要自己负责当前返回对象的后继生命周期的管理工作,包括该对象的销毁。
- request、session和global session:这三个scope类型是Spirng 2.0之后新增加的,它们不像之前的singleton和prototype那么“通用”,因为它们只适用于Web应用程序,通常是与XmlWebApplicationContext共同使用。三者的作用域顾名思义,分别对应web应用的request、session和global session。
4.2.1.2 FactoryBean
FactoryBean是我们接触到的第一个特殊bean,首先它是一个Bean(这表示spring bean的定义它都有),但又不仅仅是一个Bean(它有特殊功能)。它是一个能生产或修饰对象生成的工厂Bean,类似于设计模式中的工厂模式和装饰器模式。它能在需要的时候“改装”一个对象,且不仅仅限于它自身,它能返回任何Bean的实例。
简而言之,factoryBean是一个bean,一个拥有简单bean工厂职能的bean。
FactoryBean是一个接口,它只定义了三个方法:
1 | public interface FactoryBean<T> { |
这三个方法最核心的 getObject()方法,其他两个方法都服务于它。
那么FactoryBean有什么作用呢??
我们知道,在spring Ioc容器中getBean的时候,底层是通过java的反射机制调用bean的构造器来new一个对象返回,如果我希望从容器中返回的对象不是新new出来的对象,而是某个我指定的对象呢??
比如我们需要从容器中获取一辆车:
1 |
|
因为反射都是调用无参构造器来new对象,所以我们只能得到一辆黑色的车,那我如果想要一辆白色的车呢?
我们现有的beanFactory只支持生产默认的黑色的车,那为了得到白色的车,我们得拥有指定想要哪台车的能力,如何指定呢?
我们知道,在对象的概念中,A extends B表示的是A是B; A implements B表示的是A有B提供的能力。我们希望我们在提车时可以自己选择自己想要的车,而FactoryBean就提供了这种能力。
Car类实现了FactoryBean,就表示告诉spring IoC:当beanFactory按照Car类的图纸(beanDefinition,下文将详细描述)来生产Car的实例的时候,如果发现我的图纸上有注明要指定我想要的汽车(即实现FactoryBean接口),那么beanFactory就得按照我的要求来生产我制定的汽车。
这时候,我们的“图纸”可以这么定义:
1 | @Component |
这时候你从容器中取出来的Car类型的实例,都会是白车了:
1 | @RunWith(SpringRunner.class) |
得到结果:
car1 = 白色
car2 = 黑色
car1.equals(car2) = false
说了这么多,为什么要有FactoryBean这个东西呢,有什么具体的作用吗?
其实FactoryBean在Spring中最为典型的一个应用就是用来创建AOP的代理对象。
我们知道AOP实际上是Spring在运行时创建了一个代理对象,也就是说这个对象,是我们在运行时创建的,而不是一开始就定义好的,这很符合工厂方法模式。更形象地说,AOP代理对象通过Java的反射机制,在运行时指定了一个定制的代理对象,在代理对象的目标方法中根据业务要求织入了相应的方法。这个对象在Spring中就是——ProxyFactoryBean。
所以,FactoryBean为我们实例化Bean提供了一个更为灵活的方式,我们可以通过FactoryBean创建出更为复杂的Bean实例。
4.2.2 BeanDefinition
在Java中,一切皆对象。在JDK中使用java.lang.Class来描述类这个对象。
在Spring中,存在bean这样一个概念,那Spring又是怎么抽象bean这个概念,用什么类来描述bean这个对象呢?Spring使用BeanDefinition来描述bean。
顾名思义,BeanDefinition就是Spring对bean的定义对象,spring从Configuration Metadata中读取bean的配置,包括它的beanName,是否是单例,具体指向哪个类,是否是懒加载,有哪些依赖等等信息,都存在BeanDefinition对象中,BeanDefinition就是beanFactory生产bean的图纸。
将bean定义成BeanDefinition后,spring对bean的操作就可以改为对BeanDefinition进行,比如拿到某个BeanDefinition后,可以根据里面的类名、构造函数、构造函数参数,使用反射进行对象创建。
BeanDefinition实现了AttributeAccessor和BeanMetadataElement接口。在Spring中充斥着大量的各种接口,每种接口都拥有不同的能力,某个类实现了某个接口,也就相应的拥有了某种能力:
- AttributeAccessor:顾名思义,这是一个属性访问者,它提供了对外访问属性的能力。
- BeanMetadataElement:提供了获取元数据元素的配置源对象的能力。
BeanDefinition的属性和方法如下图所示,大部分方法/属性的作用都能简单从名字区分出来,部分方法的作用,我们下面来简单介绍。
1 | //用于描述一个具体bean实例 |
BeanDefinition接口有诸多的实现类,不同的实现类,使用的场景也不尽相同:
- AbstractBeanDefinition,是BeanDefinition的主要实现类,也是所有bean定义的父类。
- RootBeanDefinition,是在XML配置时代,注册bean定义时用的类。
- ChildBeanDefinition,是在XML配置时代,注册bean定义时用的类,必须在配置时指定一个父bean定义。
- GenericBeanDefinition,在注解配置时代,推荐使用的bean定义类,可以在运行时动态指定一个父bean定义,也可以不指定。
- AnnotatedGenericBeanDefinition,在注解配置时代,通过编程方式注册bean定义时用的类,继承了GenericBeanDefinition。
- ScannedGenericBeanDefinition,在注解配置时代,通过扫描jar包中.class文件的方式注册bean定义时用的类,继承了GenericBeanDefinition。
4.3 Spring IoC容器流程
Spring的IoC容器所起的作用,就像下图所展示的那样,它会以某种方式加载Configuration Metadata(通常也就是XML格式的配置信息),然后根据这些信息绑定整个系统的对象,最终组装成一个可用的基于轻量级容器的应用系统。
Spring的IoC容器实现以上功能的过程,基本上可以按照类似的流程划分为两个阶段,即容器启动阶段和Bean实例化阶段
4.3.1 容器启动阶段
容器启动伊始,首先会通过某种途径加载Configuration MetaData。除了代码方式比较直接,在大部分情况下,容器需要依赖某些工具类(BeanDefinitionReader)对加载的Configuration MetaData。
对Configuration MetaData进行解析和分析,并将分析后的信息编组为相应的BeanDefinition,最后把这些保存了bean定义必要信息的BeanDefinition,注册到相应的BeanDefinitionRegistry
总地来说,该阶段所做的工作可以认为是准备性的,重点更加侧重于对象管理信息的收集。
4.3.2 Bean实例化阶段
经过第一阶段,现在所有的bean定义信息都通过BeanDefinition的方式注册到了BeanDefinitionRegistry中。当某个请求方通过容器的getBean方法明确地请求某个对象,或者因依赖关系容器需要隐式地调用getBean方法时,就会触发第二阶段的活动。
容器会首先检查所请求的对象之前是否已经实例化和初始化。如果没有,则会根据注册的BeanDefinition所提供的信息实例化被请求对象,并为其注入依赖,然后初始化。如果该对象实现了某些回调接口,也会根据回调接口的要求来装配它。
当该对象装配完毕之后,容器会立即将其返回请求方使用。
如果说第一阶段只是根据图纸装配生产线的话,那么第二阶段就是使用装配好的生产线来生产具体的产品了
注意,我们上面提高的bean的实例化,注入依赖(或者叫依赖装配),初始化,是三个递进的不同阶段,注意区分。
4.4 spring bean的生命周期
确的了解Spring Bean的生命周期是非常必要的。我们通常使用ApplicationContext作为Spring容器。这里,我们讲的也是 ApplicationContext中Bean的生命周期。而实际上BeanFactory也是差不多的,只不过处理器需要手动注册。
开门见山,我们先直接给出一张总图,然后再分别描述:
可以看到,Bean的完整生命周期经历了各种方法调用,这些方法可以划分为以下几类:
Bean自身的方法:这个包括了Bean本身调用的方法(如构造器,依赖注入的set方法等)和通过配置文件中
<bean>
的init-method
和destroy-method
指定的方法。Bean级生命周期接口方法:这个包括了Aware接口的相关实现类(如BeanNameAware、BeanFactoryAware)以及InitializingBean、DiposableBean这些接口的方法。
容器级生命周期接口方法:
- Bean后处理器接口方法:所有实现了BeanPostProcessor这个接口的实现类,一般称它们为“后处理器”,或者“bean后处理器”。主要作用是对容器中的bean进行后处理,也就是额外的加强。(注意,它的作用对象是它所注册的容器中的所有收管bean)
- 工厂后处理器接口方法:所有实现了BeanFactoryPostProcessor这个接口的实现类,一般称它们为“工厂后处理器”,或者“容器后处理器”。主要作用是对IoC容器进行后处理,增强容器功能。(注意,它的作用对象是它所注册的容器的对象)
这些类或接口叫做Hook类/接口,这些接口/类的存在,使得Spring Framework具有非常高的扩展性,使得我们可以在bean的生命周期的关键节点介入,得到一些我们需要的信息,或者做一些对bean的“改装”。
第一类,Bean自身的方法,这个不再赘述,我们从bean的生命周期中各类的调用顺序,来依次介绍二三类的这些接口方法:
4.4.1 Bean级生命周期接口方法
4.4.1.1 InitializingBean/DisposableBean接口方法
InitializingBean和DisposableBean接口十分的简单:
1 | public interface InitializingBean { |
1 | public interface DisposableBean { |
功能也十分简单:如果想在bean的 创建/销毁 过程中做一些骚操作的话,就实现这两个接口中对应的接口方法,将骚操作逻辑定义在里面。
Spring在创建/销毁bean的过程中,会判断bean是否实现了这二者的接口方法,如果实现了,就在适当的时机调用它。
具体逻辑见AbstractAutowireCapableBeanFactory#initializeBean()和AbstractAutowireCapableBeanFactory#invokeInitMethods()
4.4.1.2 Aware接口方法
Spring中有很多继承于aware接口的类,如下图,那么这些类到底是做什么用到的呢??
有些时候,我们需要在bean的实例化过程中,获取bean的某些信息来做一些工作,这些信息包括bean的beanName,构造这个bean的ApplicationContext,加载这个bean类的beanClassLoader等等。
假设我们有一个Car类,我们希望在bean的初始化过程中能够有机会获取到bean的beanName,以便我们把beanName赋值给carName,让每辆car的carName和beanName一致,那么我们可以这么做:
1 | public class Car implements BeanNameAware{ //让Car实现BeanNameAware |
这时我们在Configuration MetaData中定义两个Car类
1 | <bean id="benchi" class="balabala.Car"> |
1 | @Autowired |
那么可以得到结果:benchi.carName=“benchi”,baoma.carName=“baoma”;
aware,翻译过来是知道的,已感知的,意识到的,所以,这些接口从字面意思是能感知到所有Aware前缀值的含义。
实际上,这些接口也确实提供了可实现的方法,在bean的实例化过程中,将各个Aware想要获取的信息通过参数的方式传到实现的方法中来,给开发者一个获取到相关信息值的机会。
如上例的Car implements BeanNameAware
,BeanNameAware定义的setBeanName(String beanName)方法,就会在实例化过程中把beanName信息传进方法中来让开发者使用,得意于此,我们才能得到beanName,并将其赋值给carName。
至于其他的Aware实现类,他们获取的信息不同,但逻辑也都是一样的。
BeanNameAware接口是为了让自身Bean能够感知到,获取到自身在Spring容器中的id(也就是beanName)属性。
实现了ApplicationContextAware接口的类,能够获取到ApplicationContext
实现了BeanFactoryAware接口的类,能够获取到BeanFactory对象。
…
在实例化过程中,将相关信息传进接口方法中以供使用,spring怎么做到这一点的呢??
其实非常简单,bean在初始化前会调用一次ApplicationContextAwareProcessor类的postProcessBeforeInitialization方法,如果bean实现了Aware接口,那么会继续判断bean实现了具体的什么接口,执行对应接口的方法:
1 | private void invokeAwareInterfaces(Object bean) { |
4.4.2 容器级生命周期接口方法
容器级生命周期接口方法主要分为Bean后处理器接口方法和工厂后处理器接口方法。前者可以对容器中的bean进行增强,后者对容器进行增强,二者我们依次介绍
4.4.2.1 Bean后处理器
BeanPostProcessor接口是所有Bean后处理器的顶层接口:
1 | public interface BeanPostProcessor { |
注意,postProcessBeforeInitialization和postProcessAfterInitialization,在接口上就已经有默认实现了,所以其他的Bean后处理器实现类,不一定要重写这两个方法。
可以看到postProcessBeforeInitialization和postProcessAfterInitialization是一组对称的方法,一个后缀是BeforeInitialization,一个后缀是AfterInitialization。注意,Initialization,初始化的意思,故而一个在初始化前,一个在初始化后。
spring bean的初始化(注意,初始化不是实例化)包含:
- 调用InitializingBean接口的afterPropertiesSet方法(如果有实现的话)。
- Configuration Metadata中的init方法,如xml配置的init-method属性指定方法,或@Bean注解注册bean定义时,设置注解initMethod属性指定的方法等。
- 使用java的注解@PostConstruct,把它标在bean的一个方法上。
而postProcessBeforeInitialization和postProcessAfterInitialization方法的调用位置就是:
bean的实例化-> bean的依赖装配 -> BeforeInitialization接口方法(初始化前) -> bean的初始化方法 -> AfterInitialization接口方法(初始化后)
那么bean后处理器如何使用呢?来,我们来自定义一个bean后处理器:
1 | @Component |
Spring在初始化bean的过程中,会优先初始化那些实现了像BeanPostProcessor这类特殊接口的bean,如果容器发现初始化的bean实现了BeanPostProcessor 接口,将会将其注册为bean后处理器。
一经注册,它对它注册的spring容器下的所有bean起作用,任何bean在初始化过程都会通过bean后处理器做额外增强操作。
作为开发者,我们可以通过实现BeanPostProcessor接口方法,来自定义后处理器类,也可以使用现成,spring为我们准备好的一些后处理器,下面我们简单介绍一些重要的bean后处理器。
4.4.2.1.1 InstantiationAwareBeanPostProcessor
InstantiationAwareBeanPostProcessor也是一个接口,注意,InstantiationAwareBeanPostProcessor的一对before和after接口方法,不是重写的BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterInitialization。
Instantiation和Initialization,还是不一样的,前者是实例化,后者是初始化,要注意区分。
InstantiationAwareBeanPostProcessor实现BeanPostProcessor接口,更多意义上是为了将自己归类进bean后处理器中,好让容器识别自己的“身份”。它的逻辑载体(即三个方法),都是自实现的。
1 | public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor { |
InstantiationAwareBeanPostProcessor的名字中有Instantiation(实例化),说明和BeanPostProcessor只能介入初始化的前后不一样,InstantiationAwareBeanPostProcessor可以介入到bean的实例化的前后,所以它的执行时机是:
bean的实例化准备阶段 -> BeforeInstantiation接口方法(实例化前)-> bean的实例化 -> AfterInstantiation接口方法(实例化后) -> PropertyValues接口方法(定制bean所需的属性值) -> bean的属性设置
4.4.2.1.2 DestructionAwareBeanPostProcessor
DestructionAwareBeanPostProcessor接口和InstantiationAwareBeanPostProcessor对应,后者负责实例化前后的增强,后者负责销毁前后的增强。
1 | public interface DestructionAwareBeanPostProcessor extends BeanPostProcessor { |
spring bean的销毁包含:
- 调用DisposableBean接口的destroy方法(如果有实现的话)。
- Configuration Metadata中的init方法,如xml配置的destroy-method属性指定方法,或@Bean注解注册bean定义时,设置注解destroyMethod属性指定的方法等。
- 使用java的注解@PreDestroy,把它标在bean的一个方法上。
执行时机就在销毁前后,不再细述。
4.4.2.1.3 SmartInstantiationAwareBeanPostProcessor
占位,知道有这么一个后处理器,可以用来修改bean类型,定制构造方法,还有获取一个早期(初始化还没执行)bean实例的引用,典型的用法是可以用来解决循环引用。
4.4.2.1.4 MergedBeanDefinitionPostProcessor
占位,知道有这么一个后处理器,这个接口的主要目的不是用来修改合并后的bean定义的,虽然也可以进行一些修改。
它主要用来进行一些自省操作,如一些检测,或在处理bean实例之前缓存一些相关的元数据。
这些作用都在第一个方法里实现。
4.4.2.2 Bean工厂后处理器
和Bean后处理器一样,Bean工厂后处理器是一种特殊的Bean,这种Bean并不对外提供服务,它甚至可以无需id属性,它主要负责对容器本身进行某些特殊的处理和增强。
BeanFactoryPostProcessor是所有工厂后处理器的顶层接口,在spring容器实例化bean的逻辑中,spring正是通过instanceof BeanFactoryPostProcessor
这一判断语句来确定一个bean是不是工厂后处理器。
如下图所示,spring提供的BeanFactoryPostProcessor实现类有很多,一些常见的功能,我们可以直接选择合适的工厂后处理器来继承或者实现,以免重复造轮子,其中BeanDefinitionRegistryPostProcessor是BeanFactoryPostProcessor最重要的一个实现类。
Spring中有两类工厂后处理器,BeanDefinitionRegistryPostProcessor和其他。其中其他里面又分为spring源生的,和我们自定义的。
BeanDefinitionRegistryPostProcessor继承自BeanFactoryPostProcessor,设计它的目的是为了使用它向容器注册额外的bean的配置信息——BeanDefinition对象。
4.4.2.2.1 自定义BeanFactoryPostProcessor
我们把spring提供的源生的Bean工厂后处理器之外的,我们自己通过实现BeanFactoryPostProcessor顶层接口的工厂后处理器称为普通工厂后处理器,或者自定义BeanFactoryPostProcessor;
我们先来看下BeanFactoryPostProcessor接口:
1 | public interface BeanFactoryPostProcessor { |
BeanFactoryPostProcessor能改变bean在实例化之前的一些原配置值,比如Scope,lazy,Primary,DependsOn,Role,Description等等。
比如我们有个单例的bean:
1 | @Component |
自定义实现BeanFactoryPostProcessor的处理器:
1 | @Component |
这样就完成了对于bean的作用域的变化。
4.4.2.2.2 BeanDefinitionRegistryPostProcessor
1 | //bean定义注册后处理器,就是用来向容器中注册bean定义的,造成的结果就是beanDefinition的数目变多。 |
多说无益,我们来看demo:
首先我们创建一个类并实现BeanDefinitionRegistryPostProcessor接口
1 | public class TestBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { |
这样就完成了往容器中添加BeanDefinition的操作。
4.4.2.2.3 源生工厂后处理器之ConfigurationClassPostProcessor
ConfigurationClassPostProcessor是Spring中非常重要的工厂后处理器,它的主要功能是参与BeanFactory的建造,在这个类中,会解析加了@Configuration的配置类,还会解析@ComponentScan、@ComponentScans注解扫描的包,以及解析@Import等注解。
ConfigurationClassPostProcessor 实现了 BeanDefinitionRegistryPostProcessor 接口,而 BeanDefinitionRegistryPostProcessor 接口继承了 BeanFactoryPostProcessor 接口,所以 ConfigurationClassPostProcessor 中需要重写 postProcessBeanDefinitionRegistry() 方法和 postProcessBeanFactory() 方法。而ConfigurationClassPostProcessor类的作用就是通过这两个方法去实现的。
具体代码逻辑,可以见该文:ConfigurationClassPostProcessor源码解析,介绍的非常的详细。
-