Spring的IoC容器是一个提供IoC支持的轻量级容器,除了基本的IoC(控制反转)支持,它作为轻量级容器还提供了IoC之外的支持。如在Spring的IoC容器之上,Spring还提供了相应的AOP框架支持、企业级服务集成等服务。Spring的IoC容器和IoC Service Provider所提供的服务之间存在一定的交集,二者的关系如图4-1所示。

image-20220403111452474

Spring提供了两种容器类型:BeanFactory和ApplicationContext。

  • BeanFactory。基础类型IoC容器,提供完整的IoC服务支持。如果没有特殊指定,默认采用 延迟初始化 策略(lazy-load)。只有当客户端对象需要访问容器中的某个受管对象的时候,才对该受管对象进行初始化以及依赖注入操作。所以,相对来说,容器启动初期速度较快,所需要的资源有限。 对于资源有限,并且功能要求不是很严格的场景,BeanFactory是比较合适的 IoC容器选择。
  • ApplicationContext。ApplicationContext在BeanFactory的基础上构建,是相对比较高级的容器实现,除了拥有BeanFactory的所有支持,ApplicationContext还提供了其他高级特性,比如事件发布、国际化信息支持等,这些会在后面详述。ApplicationContext所管理的对象,在该类型容器启动之后,默认 全部初始化并绑定完成 。所以,相对于BeanFactory来说,ApplicationContext要求更多的系统资源,同时,因为在启动时就完成所有初始化,容器启动时间较之BeanFactory也会长一些。 在那些系统资源充足,并且要求更多功能的场景中,ApplicationContext类型的容器是比较合适的选择。

通过图4-2,我们可以对BeanFactory和ApplicationContext之间的关系有一个更清晰的认识。

image-20220403111857783

BeanFactory,顾名思义,就是生产Bean的工厂。作为Spring提供的基本的IoC容器,BeanFactory可以完成作为IoCServiceProvider的所有职责,包括业务对象的注册对象间依赖关系的绑定

BeanFactory的定义:

  public interface BeanFactory {
        String FACTORY_BEAN_PREFIX = "&";
 
        Object getBean(String name) throws BeansException;
 
        Object getBean(String name, Class requiredType) throws BeansException;
 
        /**
         * @since 2.5
         */
        Object getBean(String name, Object[] args) throws BeansException;
 
        boolean containsBean(String name);
 
        boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
 
        /**
         * @since 2.0.3
         */
        boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
 
        /**
         * @since 2.0.1
         */
        boolean isTypeMatch(String name, Class targetType) throws NoSuchBeanDefinitionException;
 
        Class getType(String name) throws NoSuchBeanDefinitionException;
 
        String[] getAliases(String name);
    }
 

上面代码中的方法基本上都是查询相关的方法,例如,取得某个对象的方法(getBean)、查询某个对象是否存在于容器中的方法(containsBean),或者取得某个bean的状态或者类型的方法等。

拥有 BeanFactory 之后的生活

针对系统和业务逻辑,该如何设计和实现当前系统不受是否引入轻量级容器的影响。对于我们的FX新闻系统,我们还是会针对系统需求,分别设计相应的接口和实现类。前后唯一的不同,就是对象之间依赖关系的解决方式改变了。之前我们的系统业务对象需要自己去“拉”(Pull)所依赖的业务对象,有了BeanFactory之类的IoC容器之后,需要依赖什么让BeanFactory为我们推过来(Push)就行了。

下面代码演示了FX新闻系统初期的设计和实现框架代码。

 
    1-设计FXNewsProvider类用于新闻处理
    public class FXNewsProvider
    {
    ...
    }
    2-设计IFXNewsListener接口抽象各个新闻社不同的新闻获取方式
    public interface IFXNewsListener
    {
    ...
    }
    以及相应实现类
    public class DowJonesNewsListener implements IFXNewsListener
    {
    ...
    }
    3-设计IFXNewsPersister接口抽象不同数据存储方式
    public interface IFXNewsPersister
    {
    ...
    }
    以及相应的实现类
    public class DowJonesNewsPersister implements IFXNewsPersister
    {
    ...
    }
 

使用IoC模式开发的业务对象现在不用自己操心如何解决相互之间的依赖关系,只需要交给BeanFactory来做。它可以 通过XML文件来注册并管理各个业务对象之间的依赖关系 ,就像下面代码所演示的那样。

    <beans>
    	<bean id="djNewsProvider" class="..FXNewsProvider">
    		<constructor-arg index="0">
        	<ref bean="djNewsListener"/>
    		</constructor-arg>
        <constructor-arg index="1">
    			<ref bean="djNewsPersister"/>
        </constructor-arg>
    		</bean>
        ...
    </beans>

在BeanFactory出现之前,我们通常会直接在应用程序的入口类的main方法中, 自己实例化相应的对象并调用之,如以下代码所示:

    FXNewsProvider newsProvider = new FXNewsProvider();
    newsProvider.getAndPersistNews();
 

不过,现在既然有了BeanFactory,我们通常只需将“生产线图纸”交给BeanFactory,让BeanFactory为我们生产一个FXNewsProvider,如以下代码所示:

 
    BeanFactory container = new XmlBeanFactory(new ClassPathResource("配置文件路径"));
    FXNewsProvider newsProvider =(FXNewsProvider)container.getBean("djNewsProvider"); newsProvider.getAndPersistNews();

或者如以下代码所示:

    ApplicationContext container = new ClassPathXmlApplicationContext("配置文件路径");
		    FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("djNewsProvider");
		    newsProvider.getAndPersistNews();

亦或如以下代码所示:

    ApplicationContext container = new FileSystemXmlApplicationContext("配置文件路径");
    FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("djNewsProvider"); newsProvider.getAndPersistNews();

当然,这只是使用BeanFactory后开发流程的一个概览而已, 具体细节请容我慢慢道来。

BeanFactory的对象注册与依赖绑定方式

上一章在介绍IoC Service Provider时,我们提到通常会有三种方式来管理这些信息。而BeanFactory几乎支持所有这些方式。

直接编码方式

其实,把编码方式单独提出来称作一种方式并不十分恰当。因为 不管什么方式,最终都需要编码才能“落实”所有信息并付诸使用。不过,通过这些代码,起码可以让我们更加清楚BeanFactory在底层是如何运作的。

下面来看一下我们的FX新闻系统相关类是如何注册并绑定的

    public class BeanFactoryDirectCode {
        public static void main(String[] args) {
          // 构造一个BeanDefinitionRegistry
          DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
          // 进行具体的对象注册和相关依赖管理并得到BeanFactory
          BeanFactory container = (BeanFactory) bindViaCode(beanRegistry);
          // 从容器的获取bean
          FXNewsProvider newsProvider = (FXNewsProvider) container.getBean("djNewsProvider");
          // 执行业务逻辑
          newsProvider.getAndPersistNews();
        }
 
        public static BeanFactory bindViaCode(BeanDefinitionRegistry registry) {
            AbstractBeanDefinition newsProvider = new RootBeanDefinition(FXNewsProvider.class, true);
            AbstractBeanDefinition newsListener = new RootBeanDefinition(DowJonesNewsListener.class, true);
            AbstractBeanDefinition newsPersister = new RootBeanDefinition(DowJonesNewsPersister.class, true);
            // 将bean定义注册到容器中
            registry.registerBeanDefinition("djNewsProvider", newsProvider);
            registry.registerBeanDefinition("djListener", newsListener);
            registry.registerBeanDefinition("djPersister", newsPersister);
            // 指定依赖关系
            // 1. 可以通过构造方法注入方式
            ConstructorArgumentValues argValues = new ConstructorArgumentValues();
            argValues.addIndexedArgumentValue(0, newsListener);
            argValues.addIndexedArgumentValue(1, newsPersister);
            newsProvider.setConstructorArgumentValues(argValues);
            // 2. 或者通过setter方法注入方式
            MutablePropertyValues propertyValues = new MutablePropertyValues();
            propertyValues.addPropertyValue(new ropertyValue("newsListener", newsListener));
            propertyValues.addPropertyValue(new PropertyValue("newPersistener", newsPersister));
            newsProvider.setPropertyValues(propertyValues);
            // 绑定完成
            return (BeanFactory) registry;
        }
    }
 

BeanFactory只是一个接口,我们最终需要一个该接口的实现来进行实际的Bean的管理,DefaultListableBeanFactory就是这么一个比较通用的BeanFactory实现类。DefaultListableBeanFactory除了间接地实现了BeanFactory接口,还实现了BeanDefinitionRegistry接口,该接口才是在BeanFactory的实现中担当Bean注册管理 的角色。基本上,BeanFactory接口只定义如何访问容器内管理的Bean的方法,各个BeanFactory的具体实现类负责具体Bean的注册以及管理工作

BeanDefinitionRegistry接口定义抽象了Bean的注册逻辑。通常情况下,具体的BeanFactory实现类会实现这个接口来管理Bean的注册。它们之间的关系如图4-3所示。

image-20220403124712189

打个比方说,BeanDefinitionRegistry就像图书馆的书架,所有的书是放在书架上的。虽然你还书或者借书都是跟图书馆(也就是BeanFactory,或许BookFactory可能更好些)打交道,但书架才是图书馆存放各类图书的地方。所以,书架相对于图书馆来说,就是它的BookDefinitionRegistry

每一个受管的对象,在容器中都会有一个BeanDefinition的实例(instance)与之相对应,该 BeanDefinition的实例负责保存对象的所有必要信息,包括其对应的对象的class类型、是否是抽象类、构造方法参数以及其他属性等。当客户端向BeanFactory请求相应对象的时候,BeanFactory会通过这些信息为客户端返回一个完备可用的对象实例。RootBeanDefinitionChildBeanDefinitionBeanDefinition的两个主要实现类。

现在,我们再来看这段绑定代码,应该就有“柳暗花明”的感觉了。

在main方法中,首先构造一个DefaultListableBeanFactory作为BeanDefinitionRegistry,然后将其交给bindViaCode方法进行具体的对象注册和相关依赖管理,然后通过bindViaCode返回的BeanFactory取得需要的对象,最后执行相应逻辑。在我们的实例里,当然就是取得FXNewsProvider进行新闻的处理。这里,再把代码贴下:

    public static void main(String[] args) {
            // 构造一个BeanDefinitionRegistry
            DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
            // 进行具体的对象注册和相关依赖管理并得到BeanFactory
            BeanFactory container = (BeanFactory) bindViaCode(beanRegistry);
            // 从容器的获取bean
            FXNewsProvider newsProvider = (FXNewsProvider) container.getBean("djNewsProvider");
            // 执行业务逻辑
            newsProvider.getAndPersistNews();
    }
 

bindViaCode方法中,首先针对相应的业务对象构造与其相对应的BeanDefinition,使用了RootBeanDefinition作为BeanDefinition的实现类。见下方代码:

    AbstractBeanDefinition newsProvider = new RootBeanDefinition(FXNewsProvider.class, true);
    AbstractBeanDefinition newsListener = new RootBeanDefinition(DowJonesNewsListener.class, true);
    AbstractBeanDefinition newsPersister = new RootBeanDefinition(DowJonesNewsPersister.class, true);

构造完成后,将这些BeanDefinition注册到通过方法参数传进来的BeanDefinitionRegistry中。见下方代码:

    // 将bean定义注册到容器中
    registry.registerBeanDefinition("djNewsProvider", newsProvider);
    registry.registerBeanDefinition("djListener", newsListener);
    registry.registerBeanDefinition("djPersister", newsPersister);

之后,因为我们的FXNewsProvider是采用的构造方法注入,所以,需要通过ConstructorArgumentValues为其注入相关依赖。在这里为了同时说明setter方法注入,也同时展示了在Spring中如何使用代码实现setter方法注入。如果要运行这段代码,需要把setter方法注入部分的4行代码注释掉。见下方代码:

 
    // 指定依赖关系
    // 1. 可以通过构造方法注入方式
    ConstructorArgumentValues argValues = new ConstructorArgumentValues();
    argValues.addIndexedArgumentValue(0, newsListener);
    argValues.addIndexedArgumentValue(1, newsPersister);
    newsProvider.setConstructorArgumentValues(argValues);
    // 2. 或者通过setter方法注入方式
    MutablePropertyValues propertyValues = new MutablePropertyValues();
    propertyValues.addPropertyValue(new ropertyValue("newsListener", newsListener));
    propertyValues.addPropertyValue(new PropertyValue("newPersistener", newsPersister));
    newsProvider.setPropertyValues(propertyValues);
 

最后,以BeanFactory的形式返回已经注册并绑定了所有相关业务对象的BeanDefinitionRegistry实例。见下方代码:

    // 绑定完成
    return (BeanFactory) registry;

这里的强制类型转换是有特定场景的。因为传入的DefaultListableBeanFactory同时实现了BeanFactoryBeanDefinitionRegistry接口,所以,这样做强制类型转换不会出现问题。但需要注意的是,单纯的BeanDefinitionRegistry是无法强制转换到BeanFactory类型的!

外部配置文件方式

Spring的IoC容器支持两种配置文件格式:Properties文件格式和XML文件格式。

采用外部配置文件时,Spring的IoC容器有一个统一的处理方式。通常情况下,需要根据不同的外部配置文件格式,给出相应的BeanDefinitionReader实现类,由BeanDefinitionReader的相应实现类负责将相应的配置文件内容读取并映射到BeanDefinition,然后将映射后的BeanDefinition注册到一个BeanDefinitionRegistry,之后,BeanDefinitionRegistry完成Bean的注册和加载

当然,大部分工作,包括解析文件格式、装配BeanDefinition之类的工作,都是由BeanDefinitionReader的相应实现类来做的,BeanDefinitionRegistry只不过负责保管而已。整个过程类似于如下代码:

    BeanDefinitionRegistry beanRegistry = <某个BeanDefinitionRegistry实现类,通常为DefaultListableBeanFactory>;
    BeanDefinitionReader beanDefinitionReader = new BeanDefinitionReaderImpl(beanRegistry); beanDefinitionReader.loadBeanDefinitions("配置文件路径");
    // 现在我们就取得了一个可用的BeanDefinitionRegistry实例

1. Properties配置格式的加载

Spring提供了org.springframework.beans.factory.support.PropertiesBeanDefinitionReader类用于Properties格式配置文件的加载,所以,我们不用自己去实现BeanDefinitionReader,只要根据该类的读取规则,提供相应的配置文件即可。

对于FXNews系统的业务对象,我们采用如下Properties文件内容进行配置加载。

 
    djNewsProvider.(class)=..FXNewsProvider
    # ----------通过构造方法注入的时候-------------
    djNewsProvider.$0(ref)=djListener
    djNewsProvider.$1(ref)=djPersister
    # ----------通过setter方法注入的时候---------
    # djNewsProvider.newsListener(ref)=djListener
    # djNewsProvider.newPersistener(ref)=djPersister
 
    djListener.(class)=..impl.DowJonesNewsListener
    djPersister.(class)=..impl.DowJonesNewsPersister
 

djNewsProvider作为beanName,后面通过.(class)表明对应的实现类是什么,实际上使用djNewsProvider.class=...的形式也是可以的,但Spring1.2.6之后不再提倡使用,而提倡使用.(class)的形式。其他两个类的注册,djListener和djPersister,也是相同的道理。

通过在表示beanName的名称后添加.$[number]后缀的形式,来表示当前beanName对应的对象需要通过构造方法注入的方式注入相应依赖对象。在这里,我们分别将构造方法的第一个参数和第二个参数对应到djListenerdjPersister。需要注意的一点,就是$0$1后面的(ref)(ref)用来表示所 依赖的是引用对象,而不是普通的类型 。如果不加(ref)PropertiesBeanDefinitionReader会将djListenerdjPersister作为简单的String类型进行注入,异常自然不可避免啦。

FXNewsProvider采用的是构造方法注入,而为了演示setter方法注入在Properties配置文件中又是一个什么样子,以便于你更全面地了解基于Properties文件的配置方式,我们在下面增加了setter方法注入的例子,不过进行了注释。实际上,与构造方法注入最大的区别就是,它不使用数字顺序来指定注入的位置,而 使用相应的属性名称来指定注入newsListenernewPersistener恰好就是我们的FXNewsProvider类中所声明的属性名称。这印证了之前在比较构造方法注入和setter方法注入方式不同时提到的差异,即 构造方法注入无法通过参数名称来标识注入的确切位置,而setter方法注入则可以通过属性名称来明确标识注入 。与在Properties中表达构造方法注入一样,同样需要注意,如果属性名称所依赖的是引用对象,那么一定不要忘了(ref)。当这些对象之间的注册和依赖注入信息都表达清楚之后,就可以将其加载到BeanFactory而付诸使用了。而这个加载过程实际上也就像我们之前总体上所阐述的那样,下方代码清单中的内容再次演示了类似的加载过程。

   public static void main(String[] args) {
      DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
      BeanFactory container = (BeanFactory)bindViaPropertiesFile(beanRegistry);
      FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("djNewsProvider");
      newsProvider.getAndPersistNews();
    }
 
    public static BeanFactory bindViaPropertiesFile(BeanDefinitionRegistry registry) {
      PropertiesBeanDefinitionReader reader =	new PropertiesBeanDefinitionReader(registry);
      reader.loadBeanDefinitions("classpath:../../binding-config.properties");
      return (BeanFactory)registry;
    }

2. XML配置格式的加载

XML配置格式是Spring支持最完整,功能最强大的表达方式。当然,一方面这得益于XML良好的语意表达能力;另一方面,就是Spring框架从开始就自始至终保持XML配置加载的统一性。Spring2.x 之前,XML配置文件采用DTD(Document Type Definition)实现文档的格式约束。2. x 之后,引入了基于XSD(XML Schema Definition)的约束方式。不过,原来的基于DTD的方式依然有效,因为从DTD转向XSD只是“形式”上的转变,所以,后面的大部分讲解还会沿用DTD的方式,只有必要时才会给出特殊说明。

如果FX新闻系统对象按照XML配置方式进行加载的话,配置文件内容如下方所示。

 
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" ➥ "http://www.springframework.org/dtd/spring-beans.dtd">
    <beans>
        <bean id="djNewsProvider" class="..FXNewsProvider">
            <constructor-arg index="0">
                <ref bean="djNewsListener" />
            </constructor-arg>
            <constructor-arg index="1">
                <ref bean="djNewsPersister" />
            </constructor-arg>
        </bean>
        <bean id="djNewsListener" class="..impl.DowJonesNewsListener"></bean>
        <bean id="djNewsPersister" class="..impl.DowJonesNewsPersister"></bean>
    </beans>
 

有了XML配置文件,我们需要将其内容加载到相应的BeanFactory实现中,以供使用,如下方代码所示。

 
    public class BeanFactoryXml {
        public static void main(String[] args) {
            DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
            BeanFactory container = (BeanFactory) bindViaXMLFile(beanRegistry);
            FXNewsProvider newsProvider = (FXNewsProvider) container.getBean("djNewsProvider");
            newsProvider.getAndPersistNews();
        }
 
        public static BeanFactory bindViaXMLFile(BeanDefinitionRegistry registry) {
            XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry);
            reader.loadBeanDefinitions("classpath:../news-config.xml");
            return (BeanFactory) registry;
            // 或者直接
            return new XmlBeanFactory(new ClassPathResource("../news-config.xml"));
        }
    }
 
 

与为Properties配置文件格式提供PropertiesBeanDefinitionReader相对应,Spring同样为XML格式的配置文件提供了现成的BeanDefinitionReader实现,即XmlBeanDefinitionReaderXmlBeanDefinitionReader负责读取Spring指定格式的XML配置文件并解析,之后将解析后的文件内容映射到相应的BeanDefinition,并加载到相应的BeanDefinitionRegistry中(在这里是DefaultListableBeanFactory)。这时,整个BeanFactory就可以放给客户端使用了。

除了提供XmlBeanDefinitionReader用于XML格式配置文件的加载,Spring还在DefaultListableBeanFactory的基础上构建了简化XML格式配置加载的XmlBeanFactory实现。从以上代码最后注释掉的一行,你可以看到使用了XmlBeanFactory之后,完成XML的加载和BeanFactory的初始化是多么简单。

注解方式

可能你没有注意到,我在提到BeanFactory所支持的对象注册与依赖绑定方式的时候,说的是BeanFactory“几乎”支持IoC Service Provider可能使用的所有方式。之所以这么说,有两个原因。

  1. 在Spring2.5发布之前,Spring框架并没有正式支持基于注解方式的依赖注入;

  2. Spring2.5发布的基于注解的依赖注入方式,如果不使用classpath-scanning功能的话,仍然部分依赖于“基于XML配置文件”的依赖注入方式。

另外,注解是Java5之后才引入的,所以, 以下内容只适用于应用程序使用了Spring2.5以及Java5或者更高版本的情况之下

如果要通过注解标注的方式为FXNewsProvider注入所需要的依赖,现在可以使用@Autowired以及@Component对相关类进行标记。下方代码演示了FXNews相关类使用指定注解标注后的情况。

    @Component
    public class FXNewsProvider {
        @Autowired
        private IFXNewsListener newsListener;
        @Autowired
        private IFXNewsPersister newPersistener;
 
        public FXNewsProvider(IFXNewsListener newsListner, IFXNewsPersister newsPersister) {
            this.newsListener = newsListner;
            this.newPersistener = newsPersister;
        }
        ...
    }
    @Component
    public class DowJonesNewsListener implements IFXNewsListener {
    ...
    }
 
 
 
    @Component
    public class DowJonesNewsPersister implements IFXNewsPersister {
    ...
    }

Tip

@Autowired是这里的主角,它的存在将告知Spring容器需要为当前对象注入哪些依赖对象。而@Component则是配合Spring2.5中新的classpath-scanning功能使用的。现在我们只要再向Spring的配置文件中增加一个“触发器”,使用@Autowired和@Component标注的类就能获得依赖对象的注入了。下方代码给出的正是针对这部分功能的配置内容。

<?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
      http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context-2.5.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
        <context:component-scan base-package="cn.spring21.project.base.package" />
    </beans>

Tip

<context:component-scan/> 会到指定的包(package)下面扫描标注有@Component 的类,如果找到,则将它们添加到容器进行管理,并根据它们在所标注的@Autowired 为这些类注入符合条件的依赖对象。

在以上所有这些工作都完成之后,我们就可以像通常那样加载配置并执行当前应用程序了,如以下代码所示:

 
    public static void main(String[] args) {
      ApplicationContext ctx = new ClassPathXmlApplicationContext("配置文件路径");
      FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("FXNewsProvider");
      newsProvider.getAndPersistNews();
    }
 

本章最后将详细讲解Spring 2.5新引入的“基于注解的依赖注入”。当前的内容只是让我们先从总体上有一个大概的印象,所以,不必强求自己现在就完全理解它们。