引言

Spring提供了两种容器类型:

  • BeanFactory
  • ApplicationContext

其中ApplicationContext间接继承自BeanFactory,两者最大的不同是容器初始化策略。

BeanFactory采用懒加载(lazy-load)策略,即当客户端需要访问容器内的某个对象时,才对该对象进行初始化以及依赖注入操作。所以该模式下,启动速度较快,适用于资源有限,对功能要求不是很严格的场景。

ApplicationContext所管理的对象,默认启动之后全部初始化并绑定完成,故启动速度较慢,但其除了拥有BeanFactory的所有支持,还提供事件发布、国际化信息支持等,所以适用于系统资源充足,并且要求更多功能的场景。

本文首先讲解BeanFactory。

BeanFactory、BeanDefinitionRegistry和Bean的关系

BeanFactory主要完成2项工作:

  • 业务对象的初始化及注册;
  • 对象间依赖关系的绑定。

看一下BeanFactory的源码:

public interface BeanFactory {
    String FACTORY_BEAN_PREFIX = "&";

    Object getBean(String var1) throws BeansException;

    <T> T getBean(String var1, @Nullable Class<T> var2) throws BeansException;

    Object getBean(String var1, Object... var2) throws BeansException;

    <T> T getBean(Class<T> var1) throws BeansException;

    <T> T getBean(Class<T> var1, Object... var2) throws BeansException;

    boolean containsBean(String var1);

    boolean isSingleton(String var1) throws NoSuchBeanDefinitionException;

    boolean isPrototype(String var1) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String var1, ResolvableType var2) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String var1, @Nullable Class<?> var2) throws NoSuchBeanDefinitionException;

    @Nullable
    Class<?> getType(String var1) throws NoSuchBeanDefinitionException;

    String[] getAliases(String var1);
}

可以发现,BeanFactory接口只定义了查询相关的方法,例如: 取得某个对象的方法(getBean)、查询某个对象是否存在于容器中的方法(containsBean),或者取得某个bean的状态或者类型的方法等。

而对象的注册管理及依赖绑定则交给BeanFactory的接口实现类来完成,如常见的DefaultListableBeanFactory。

public class DefaultListableBeanFactory 
	extends AbstractAutowireCapableBeanFactory 
	implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
	...
}

DefaultListableBeanFactory还实现了BeanDefinitionRegistry接口,来完成Bean的注册管理。

简单阐释一下BeanFactory、BeanDefinitionRegistry和Bean的关系:

Bean是图书,BeanFactory相当于图书馆,而BeanDefinitionRegistry则相当于图书馆的书架。虽然还书和借书均是跟图书馆(BeanFactory)打交道,但是图书馆实际存储书的地方是书架(BeanDefinitionRegistry)。

每个Bean交给BeanFactory管理时,均会包装成BeanDefination接口的实例,BeanDefination实例负责保存Bean所有必要的信息,包括Class类型、是否是抽象类、构造方法参数及其他属性等。

public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {
    String SCOPE_SINGLETON = "singleton";
    String SCOPE_PROTOTYPE = "prototype";
    int ROLE_APPLICATION = 0;
    int ROLE_SUPPORT = 1;
    int ROLE_INFRASTRUCTURE = 2;

    void setParentName(@Nullable String var1);

    @Nullable
    String getParentName();

    void setBeanClassName(@Nullable String var1);

    @Nullable
    String getBeanClassName();

    void setScope(@Nullable String var1);

    @Nullable
    String getScope();

    void setLazyInit(boolean var1);

    boolean isLazyInit();

    void setDependsOn(@Nullable String... var1);

    @Nullable
    String[] getDependsOn();

    void setAutowireCandidate(boolean var1);

    boolean isAutowireCandidate();

    void setPrimary(boolean var1);

    boolean isPrimary();

    void setFactoryBeanName(@Nullable String var1);

    @Nullable
    String getFactoryBeanName();

    void setFactoryMethodName(@Nullable String var1);

    @Nullable
    String getFactoryMethodName();

    ConstructorArgumentValues getConstructorArgumentValues();

    default boolean hasConstructorArgumentValues() {
        return !this.getConstructorArgumentValues().isEmpty();
    }

    MutablePropertyValues getPropertyValues();

    default boolean hasPropertyValues() {
        return !this.getPropertyValues().isEmpty();
    }

    boolean isSingleton();

    boolean isPrototype();

    boolean isAbstract();

    int getRole();

    @Nullable
    String getDescription();

    @Nullable
    String getResourceDescription();

    @Nullable
    BeanDefinition getOriginatingBeanDefinition();
}

RootBeanDefinition和ChildBeanDefinition是BeanDefinition的两个主要实现类。

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

假设我们有一家饭店,需要有蔬菜采购和厨师做菜,则Restaurant类定义如下:

public class Restaurant {
    // 厨师
    private Chef chef;
    // 蔬菜
    private Vegetable vegetable;

    构造方法...
	get/set方法...
}

蔬菜存在多种,我们抽象出Vegetable接口,并存在购买行为。

public interface Vegetable {
    public String buy();
}

编写2个Vegetable接口的实现类Tomato、Cabbage:

public class Tomato implements Vegetable {
    @Override
    public String buy() {
        System.out.println("采购员买来了番茄!");
        return "Tomato";
    }
}

public class Cabbage implements Vegetable {
    @Override
    public String buy() {
        System.out.println("采购员买来了卷心菜!");
        return "Cabbage";
    }
}

厨师也可能有多种,抽象出Chef接口如下:

public interface Chef {
    public void cook(String vegetableName);
}

并编写2个Chef的实现类:

public class ChineseChef implements Chef {
    @Override
    public void cook(String vegetableName) {
        System.out.println("中国厨师正在做" + vegetableName + "!");
    }
}

public class ForeignChef implements Chef {

    @Override
    public void cook(String vegetableName) {
        System.out.println("外国厨师正在做" + vegetableName + "!");
    }
}

假设我们现在要开这样一家饭店,需要中国厨师和番茄,那么如何完成对象的注册管理和依赖绑定呢?

直接编码形式

所谓直接编码形式,即用Spring的底层容器类来进行Bean的注册和管理,虽然不常用,但却有助于我们了解Spring的IOC是如何运作的。

public class Test {
    public static void main(String[] args) {
        DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
        BeanFactory container = bindViaCode(beanRegistry);
        // 测试Bean的装配
        Restaurant chineseRestaurant = (Restaurant) container.getBean("chineseRestaurant");
        Vegetable vegetable = chineseRestaurant.getVegetable();
        String vegetableName = vegetable.buy();
        Chef chef = chineseRestaurant.getChef();
        chef.cook(vegetableName);
        // 测试Bean的管理
        Chef chef1 = (Chef) container.getBean("chineseChef");
        chef1.cook("干锅花菜");
    }

    public static BeanFactory bindViaCode(BeanDefinitionRegistry registry){
        AbstractBeanDefinition restaurant = new RootBeanDefinition(Restaurant.class);
        AbstractBeanDefinition chef = new RootBeanDefinition(ChineseChef.class);
        AbstractBeanDefinition vegetable = new RootBeanDefinition(Tomato.class);
        // 将Bean注册到容器中
        registry.registerBeanDefinition("chineseRestaurant", restaurant);
        registry.registerBeanDefinition("chineseChef", chef);
        registry.registerBeanDefinition("tomato", vegetable);
        // 1. 指定依赖关系(构造方法注入)
        ConstructorArgumentValues argValues = new ConstructorArgumentValues();
        argValues.addIndexedArgumentValue(0, chef);
        argValues.addIndexedArgumentValue(1, vegetable);
        restaurant.setConstructorArgumentValues(argValues);
        // 2. 指定依赖关系(setter方法注入)
        MutablePropertyValues propertyValues = new MutablePropertyValues();
        propertyValues.addPropertyValue(new PropertyValue("chef", chef));
        propertyValues.addPropertyValue(new PropertyValue("vegetable", vegetable));
        restaurant.setPropertyValues(propertyValues);
        // 绑定完成
        return (BeanFactory) registry;
    }
}

运行结果如下:

采购员买来了番茄!
中国厨师正在做Tomato!
中国厨师正在做干锅花菜!

外部配置文件形式

其实就是将Bean的依赖关系放在配置文件中,然后Spring解析配置文件,得到各对象的依赖关系,进而完成对象的注册和依赖管理。

整体的流程如下:

  1. BeanDefinitionReader实现类读取配置文件内容,并映射得到待管理Bean的BeanDefinition;
  2. BeanDefinitionReader根据配置文件内容定义的依赖关系,通过BeanDefinition指定Bean之间的依赖关系;
  3. 将映射后的BeanDefinition注册到BeanDefinitionRegistry中。

上述大部分的工作,如解析文件格式、装配BeanDefinition之类的工作,均由BeanDefinitionReader实现类来完成,BeanDefinitionRegistry仅是负责Bean的保管而已。

整个过程类似于如下代码:

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

看一下BeanDefinitionReader:

public interface BeanDefinitionReader {
    BeanDefinitionRegistry getRegistry();

    @Nullable
    ResourceLoader getResourceLoader();

    @Nullable
    ClassLoader getBeanClassLoader();

    BeanNameGenerator getBeanNameGenerator();

    int loadBeanDefinitions(Resource var1) throws BeanDefinitionStoreException;

    int loadBeanDefinitions(Resource... var1) throws BeanDefinitionStoreException;

    int loadBeanDefinitions(String var1) throws BeanDefinitionStoreException;

    int loadBeanDefinitions(String... var1) throws BeanDefinitionStoreException;
}

可以看出,BeanDefinitionReader通过getRegistry()获取BeanDefinitionRegistry,getResourceLoader()加载配置文件资源用于解析Bean的注册和依赖关系,getBeanClassLoader()获取类加载器用于类的加载,loadBeanDefinitions(…)方法主要运用反射将待管理的Bean封装成BeanDefinition,最后注册到BeanDefinitionRegistry中。

由上图可以看出,Spring的IOC容器默认支持3种文件格式:

  • Properties文件格式
  • XML文件格式
  • Groovy文件格式

本文主要讨论前2种文件格式:

Properties文件

首先在resources目录下创建beans.properties文件:

# bean的name.(class)=实现类的全路径
chef.(class)=com.ruanshubin.springboot.ioc.service.impl.ChineseChef
vegetable.(class)=com.ruanshubin.springboot.ioc.service.impl.Tomato
restaurant.(class)=com.ruanshubin.springboot.ioc.domain.Restaurant
# 通过构造方法注入
restaurant.$0(ref)=chef
restaurant.$1(ref)=vegetable
# 通过setter方法注入
# restaurant.setChef(ref)=chef
# restaurant.setVegetable(ref)=vegetable

然后编写主函数:

public class FileTest {
    public static void main(String[] args) {
        DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
        BeanFactory container = bindViaPropertiesFile(beanRegistry);
        // 测试Bean的装配
        Restaurant chineseRestaurant = (Restaurant) container.getBean("restaurant");
        Vegetable vegetable = chineseRestaurant.getVegetable();
        String vegetableName = vegetable.buy();
        Chef chef = chineseRestaurant.getChef();
        chef.cook(vegetableName);
        // 测试Bean的管理
        Chef chef1 = (Chef) container.getBean("chef");
        chef1.cook("蒜蓉空心菜!");
    }

    public static BeanFactory bindViaPropertiesFile(BeanDefinitionRegistry registry){
        PropertiesBeanDefinitionReader reader = new PropertiesBeanDefinitionReader(registry);
        reader.loadBeanDefinitions("classpath:beans.properties");
        return (BeanFactory) registry;
    }
}

运行结果如下:

采购员买来了番茄!
中国厨师正在做Tomato!
中国厨师正在做蒜蓉空心菜!
XML文件

XML配置格式是Spring支持最完整,功能最强大的表达方式,主要得益于:

  • XML良好的语意表达能力;
  • Spring框架从开始就自始至终保持XML配置加载的统一性。

Spring 2.x之前,XML配置文件采用DTD(Document Type Definition)实现文档的格式约束。2.x之后,引入了基于XSD(XML Schema Definition)的约束方式。不过,原来的基于DTD的方式依然有效,因为从DTD转向XSD只是“形式”上的转变。

首先在resources目录下创建beans.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="restaurant" class="com.ruanshubin.springboot.ioc.domain.Restaurant">
        <constructor-arg index="0">
            <ref bean="chef"/>
        </constructor-arg>
        <constructor-arg index="1">
            <ref bean="vegetable"/>
        </constructor-arg>
    </bean>

    <bean id="chef" class="com.ruanshubin.springboot.ioc.service.impl.ForeignChef"/>

    <bean id="vegetable" class="com.ruanshubin.springboot.ioc.service.impl.Cabbage"/>
</beans>

主函数:

public class FileTest {
    public static void main(String[] args) {
        DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
        BeanFactory container = bindViaXmlFile(beanRegistry);
        // 测试Bean的装配
        Restaurant chineseRestaurant = (Restaurant) container.getBean("restaurant");
        Vegetable vegetable = chineseRestaurant.getVegetable();
        String vegetableName = vegetable.buy();
        Chef chef = chineseRestaurant.getChef();
        chef.cook(vegetableName);
        // 测试Bean的管理
        Chef chef1 = (Chef) container.getBean("chef");
        chef1.cook("法式焗蜗牛");

    }

    public static BeanFactory bindViaXmlFile(BeanDefinitionRegistry registry){
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry);
        reader.loadBeanDefinitions("classpath:beans.xml");
        return (BeanFactory) registry;
		// 或者直接如下:
        // return new XmlBeanFactory(new ClassPathResource("beans.xml"));
    }
}

运行结果如下:

采购员买来了卷心菜!
外国厨师正在做Cabbage!
外国厨师正在做法式焗蜗牛!

当然,如果你想使用其他格式的配置文件来进行Bean的注册和依赖管理,可以实现自定义的BeanDefinitionReader来达到自定义文件加载的目的。

注解形式

基于注解的方式,主要是通过@Autowired及@Component对相关类进行标记,然后通过AbstractApplicationContext借助classpath-scanning功能来完成Bean的注册和依赖管理。

首先为相关类添加注解:

@Component
public class Restaurant {
    // 厨师
    @Autowired
    private Chef chef;
    // 蔬菜
    @Autowired
    private Vegetable vegetable;
	...
	}
	
@Component
public class Tomato implements Vegetable {
	...
}

@Component
public class ChineseChef implements Chef {
	...
}

@Autowired用于依赖注入,@Component用于Bean注册,接下来配置注解扫描。

在resources目录下新建spring.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"
        xmlns:context="http://www.springframework.org/schema/context"
        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">

    <context:component-scan base-package="com.ruanshubin.springboot.ioc"/>

</beans>

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

主函数:

public class AnnotationTest {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("spring.xml");
        Restaurant chineseRestaurant = (Restaurant) ctx.getBean("restaurant");
        Vegetable vegetable = chineseRestaurant.getVegetable();
        String vegetableName = vegetable.buy();
        Chef chef = chineseRestaurant.getChef();
        chef.cook(vegetableName);
        // 测试Bean的管理
        Chef chef1 = (Chef) ctx.getBean("chineseChef");
        chef1.cook("鸡蛋韭菜水饺");
    }
}

运行结果为:

采购员买来了番茄!
中国厨师正在做Tomato!
中国厨师正在做鸡蛋韭菜水饺!

我们再配置Bean时,只是在实体类上加了@Component注解,并没有指定Bean的id或name,ApplicationContext默认采用实体类的小驼峰作为id,如ChineseChef的id为chineseChef,Tomato的id为tomato。

同时,在配置Bean依赖时,@Autowired加在接口之上,如:

@Autowired
private Chef chef;

由于我们只在ChineseChef添加了@Component注解,所以Spring容器里只有1个Chef的实现类,此时,ApplicationContext直接将ChineseChef注入到Restaurant中。

假如Spring容器里有2个Chef的实现类.会出现什么情况呢?

只需要在ForeignChef上添加@Component即可。

@Component
public class ForeignChef implements Chef {
	...
	}

重新运行主函数,报错:

Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'restaurant': Unsatisfied dependency expressed through field 'chef'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.ruanshubin.springboot.ioc.service.Chef' available: expected single matching bean but found 2: chineseChef,foreignChef
	...

错误信息很明显,说进行Restaurant依赖注入的时候,发现Chef接口有2个实现类:chineseChef、foreignChef,程序不知道该选择哪个进行注入。

解决上述错误的方式有很多,后面我们专门用1篇文章进行讲解。

参考文献:

  1. 王福强. Spring揭秘[M]. 2009.
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐