spring容器

Spring容器并不是只有一个。 Spring自带了多个容器实现, 可以归为两种不同的类型

容器是Spring框架最核心的部分,它管理着Spring应用中bean的创建、配置和管理。在该模块 中,包括了Spring bean工厂,它为Spring提供了DI的功能。基于bean工厂,我们还会发现有多 种Spring应用上下文的实现,每一种都提供了配置Spring的不同方式。

1、bean工厂(由org.springframework. beans. factory.eanFactory接口定义) 是最简单的容器, 提供基本的DI支持。

2、应用上下文(由org.springframework.context.ApplicationContext接口定义) 基于BeanFactory构建, 并提供应用框架级别的服务, 例如从属性文件解析文本信息以及发布应用事件给感兴趣的事件监听者

在bean工厂和应用上下文之间任选一种, 但bean工厂对大多数应用来说往往太低级了, 因此, 应用上下文要比bean工厂更受欢迎。 我们会把精力集中在应用上下文的使用上, 不再浪费时间讨论bean工厂

常见应用上下文:

  • AnnotationConfigApplicationContext:从一个或多个基于Java的配置类中加
    载Spring应用上下文。
  • AnnotationConfigWebApplicationContext:从一个或多个基于Java的配置类中
    加载Spring Web应用上下文。
  • ClassPathXmlApplicationContext:从类路径下的一个或多个XML配置文件中加
    载上下文定义, 把应用上下文的定义文件作为类资源 。
  • FileSystemXmlApplicationContext:从文件系统下的一个或多个XML配置文件
    中加载上下文定义
  • XmlWebApplicationContext:从Web应用下的一个或多个XML配置文件中加载上
    下文定义。

如果你想从Java配置中加载应用上下文, 那么可以使用AnnotationConfigApplicationContext:

ApplicationContext context=new AnnotationConfiApplicationContext(com.springination.zhangzhengkaiConfig.class);

应用上下文准备就绪之后, 我们就可以调用上下文的getBean()方法从Spring容器中获取bean。

Bean的生命周期

spring生命周期

在bean准备就绪之前, bean工厂执行了若干启动步骤。 我们对图1.5进行详细描
述:

1. Spring对bean进行实例化;
2. Spring将值和bean的引用注入到bean对应的属性中;
3. 如果bean实现了BeanNameAware接口, Spring将bean的ID传递给setBean-Name()方法;
4. 如果bean实现了BeanFactoryAware接口, Spring将调用setBeanFactory()方法, 将BeanFactory容器实例传入;
5. 如果bean实现了ApplicationContextAware接口, Spring将调用setApplicationContext()方法, 将bean所在的应用上下文的引用传入进来;
6. 如果bean实现了BeanPostProcessor接口, Spring将调用它们的postProcessBeforeInitialization()方法;
7. 如果bean实现了InitializingBean接口, Spring将调用它们的afterPropertiesSet()方法。 类似地, 如果bean使用init-method声明了初始化方法, 该方法也会被调用;
8. 如果bean实现了BeanPostProcessor接口, Spring将调用它们的postProcessAfterInitialization()方法;
9. 此时, bean已经准备就绪, 可以被应用程序使用了, 它们将一直驻留在应用上下文中,直到该应用上下文被销毁;
10. 如果bean实现了DisposableBean接口, Spring将调用它的destroy()接口方法。 同样, 如果bean使用destroy-method声明了销毁方法, 该方法也会被调用。

IoC容器

BeanDefinition对象:容器中的每一个bean都会有一个对应的BeanDefinition实例,该实例负责保存bean对象的所有必要信息,包括bean对象的class类型、是否是抽象类、构造方法和参数、其它属性等等。当客户端向容器请求相应对象时,容器就会通过这些信息为客户端返回一个完整可用的bean实例。
BeanDefinitionRegistryBeanFactory:BeanDefinitionRegistry抽象出bean的注册逻辑,而BeanFactory则抽象出了bean的管理逻辑,而各个BeanFactory的实现类就具体承担了bean的注册以及管理工作。它们之间的关系就如下图:IOC容器

IoC容器负责管理容器中所有bean的生命周期,而在bean生命周期的不同阶段,Spring提供了不同的扩展点来改变bean的命运。

1、在容器的启动阶段,BeanFactoryPostProcessor允许我们在容器实例化相应对象之前,对注册到容器的BeanDefinition所保存的信息做一些额外的操作,比如修改bean定义的某些属性或者增加其他信息等。如果要自定义扩展类,通常需要实现org.springframework.beans.factory.config.BeanFactoryPostProcessor接口,与此同时,因为容器中可能有多个BeanFactoryPostProcessor,可能还需要实现org.springframework.core.Ordered接口,以保证BeanFactoryPostProcessor按照顺序执行。Spring提供了为数不多的BeanFactoryPostProcessor实现

我们以PropertyPlaceholderConfigurer来说明其大致的工作流程:

在Spring项目的XML配置文件中,经常可以看到许多配置项的值使用占位符,而将占位符所代表的值单独配置到独立的properties文件,这样可以将散落在不同XML文件中的配置集中管理,而且也方便运维根据不同的环境进行配置不同的值。这个非常实用的功能就是由PropertyPlaceholderConfigurer负责实现的。

根据前文,当BeanFactory在第一阶段加载完所有配置信息时,BeanFactory中保存的对象的属性还是以占位符方式存在的,比如${jdbc.mysql.url}。当PropertyPlaceholderConfigurer作为BeanFactoryPostProcessor被应用时,它会使用properties配置文件中的值来替换相应的BeanDefinition中占位符所表示的属性值。当需要实例化bean时,bean定义中的属性值就已经被替换成我们配置的值。当然其实现比上面描述的要复杂一些。

2、BeanPostProcessor,其存在于对象实例化阶段。跟BeanFactoryPostProcessor类似,它会处理容器内所有符合条件并且已经实例化后的对象。

总结:BeanFactoryPostProcessor处理bean的定义,而BeanPostProcessor则处理bean完成实例化后的对象

BeanPostProcessor定义了两个接口:

public interface BeanPostProcessor {
    // 前置处理
    Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
    // 后置处理
    Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}

postProcessBeforeInitialization()方法与postProcessAfterInitialization()分别对应图中前置处理和后置处理两个步骤将执行的方法。这两个方法中都传入了bean对象实例的引用,为扩展容器的对象实例化过程提供了很大便利,在这儿几乎可以对传入的实例执行任何操作。注解、AOP等功能的实现均大量使用了BeanPostProcessor,比如有一个自定义注解,你完全可以实现BeanPostProcessor的接口,在其中判断bean对象的脑袋上是否有该注解,如果有,你可以对这个bean实例执行任何操作。

再来看一个更常见的例子,在Spring中经常能够看到各种各样的Aware接口,其作用就是在对象实例化完成以后将Aware接口定义中规定的依赖注入到当前实例中。比如最常见的ApplicationContextAware接口,实现了这个接口的类都可以获取到一个ApplicationContext对象。当容器中每个对象的实例化过程走到BeanPostProcessor前置处理这一步时,容器会检测到之前注册到容器的ApplicationContextAwareProcessor,然后就会调用其postProcessBeforeInitialization()方法,检查并设置Aware相关依赖。看看代码吧,是不是很简单:

// 代码来自:org.springframework.context.support.ApplicationContextAwareProcessor
// 其postProcessBeforeInitialization方法调用了invokeAwareInterfaces方法
private void invokeAwareInterfaces(Object bean) {
    if (bean instanceof EnvironmentAware) {
        ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
    }
    if (bean instanceof ApplicationContextAware) {
        ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
    }
    // ......
}

装配Bean

装配:创建应用组件之间协作的行为称为装配

Spring容器负责创建应用程序中的bean并通过DI来协调这些对象之间的关系。

Spring具有非常大的灵活性, 它提供了三种主要的装配机制:

  • 在XML中进行显式配置。
  • 在Java中进行显式配置
  • 隐式的bean发现机制和自动装配。

自动化装配Bean

Spring从两个角度来实现自动化装配:
组件扫描(component scanning):Spring会自动发现应用上下文中所创建的bean。
自动装配(autowiring):Spring自动满足bean之间的依赖 。

注解详解:

@Component注解。 这个简单的注解表明该类会作为组件类, 并告知Spring要为这个类创建bean

不过, 组件扫描默认是不启用的。 我们还需要显式配置一下Spring, 从而命令它去寻找带有@Component注解的类, 并为其创建bean。

@ComponentScan注解, 这个注解能够在Spring中启用组件扫描。

@ComponentScan注解下没有其他配置的话, @ComponentScan默认会扫描与配置类相同的包以及子包

---对应Xml配置中的:< context:component-scan >元素

@Autowired注解:在满足依赖的过程中, 会在Spring应用上下文中寻找匹配某个bean需求的其他bean。 为了声明要进行自动装配, 我们可以借助Spring的@Autowired注解 ;@Autowired注解可以用在类的任何方法上

特别注意:bean装载到容器中时,如果没有指定名称,会自动的将其名称作为bean名称(首字母大写会变成小写)

通过Java装配Bean

尽管在很多场景下通过组件扫描和自动装配实现Spring的自动化配置是更为推荐的方式, 但有时候自动化配置的方案行不通, 因此需要明确配置Spring。 比如说, 你想要将第三方库中的组件装配到你的应用中, 在这种情况下, 是没有办法在它的类上添加@Component和@Autowired注解的, 因此就不能使用自动化装配的方案了。

显式配置有俩种方案:

  • 在XML中进行显式配置
  • 在Java中进行显式配置

注解详解:

@Configuration注解:表明这个类是一个配置类, 该类应该包含在Spring应用上下文中如何创建bean的细节。

@Bean注解:会告诉Spring这个方法将会返回一个对象, 该对象要注册为Spring应用上下文中的bean。

@Configuration注解中是包含@Component注解的,被@Configuration修饰的类被定义为一个Spring容器(应用上下文)

@Configuration就相当于Spring配置文件中的<beans />标签,里面可以配置bean

@Configuration 中所有带 @Bean 注解的方法都会被动态代理,因此调用该方法返回的都是同一个实例。

其工作原理是:如果方式是首次被调用那么原始的方法体会被执行并且结果对象会被注册到Spring上下文中,之后所有的对该方法的调用仅仅只是从Spring上下文中取回该对象返回给调用者。

@Component中进行对象调用时,如果进行new一个对象,由于只是纯JAVA方式的调用,则多次调用该方法返回的是不同的对象实例。

通过Xml装配Bean

< beans >是该模式中的一个元素, 它是所有Spring配置文件的根元素。

< bean >元素类似于JavaConfig中的@Bean注解 ;创建这个bean的类通过class属性来指定的, 并且要使用全限
定的类名。

< constructor-arg >元素 ;使用构造器注入到bean的引用;用ref属性引用其他bean,也可以通过value属性,通过该属性表明给定的值要以字面量的形式注入到构造器中

< set >和< list >元素的区别不大, 其中最重要的不同在于当Spring创建要装配的集合时,所创建的是java.util.Set还是java.util.List。 如果是Set的话, 所有重复的值都会被忽略掉

< property >元素为属性的Setter方法所提供的功能与< constructor-arg >元素为构造器所提供的功能是一样的。

Bean的作用域

作用域

  • 单例(Singleton) : 在整个应用中, 只创建bean的一个实例。(默认)
  • 原型(Prototype) : 每次注入或者通过Spring应用上下文获取的时候, 都会创建一个新的bean实例。
  • 会话(Session) : 在Web应用中, 为每个会话创建一个bean实例。
  • 请求(Rquest) : 在Web应用中, 为每个请求创建一个bean实例。

如果选择其他的作用域, 要使用@Scope注解, 它可以与@Component@Bean一起使用。

使用ConfigurableBeanFactory类的SCOPE_PROTOTYPE常量设置了原型作用域。 你当然也可以使@Scope("prototype"), 但是使用SCOPE_PROTOTYPE常量更加安全并且不易出错。

@Component
@Scope(ConfigurableBeaanFactory.SCOPE.PROTOTYPE)
public class zzkai{...}

如果你使用XML来配置bean的话, 可以使用< bean >元素的scope属性来设置作用域:

会话作用域bean的作用示例: 在典型的电子商务应用中, 可能会有一个bean代表用户的购物车。 如果购物车是单例的话, 那么将会导致所有的用户都会向同一个购物车中添加商品。 另一方面, 如果购物车是原型作用域的, 那么在应用中某一个地方往购物车中添加商品, 在应用的另外一个地方可能就不可用了, 因为在这里注入的是另外一个原型作用域的购物车。就购物车bean来说, 会话作用域是最为合适的, 因为它与给定的用户关联性最大。

作用域代理问题

@Scope同时还有一个proxyMode属性, 它被设置成了ScopedProxyMode.INTERFACES。 这个属性解决了将会话或请求作用域的bean注入到单例bean中所遇到的问题。

proxyMode属性解决的问题

假设我们要将ShoppingCart bean注入到单例StoreService bean的Setter方法中, 如下所示:

@Component
public class StoreService{
    @AutoWired
    public void setShoppingCart(ShoppingCart shoppingCart){
        this.shoppingCart=shoppingCart;
    }
}

因为StoreService是一个单例的bean, 会在Spring应用上下文加载的时候创建。 当它创建的时候, Spring会试图将ShoppingCart bean注入到setShoppingCart()方法中。 但是ShoppingCart bean是会话作用域的, 此时并不存在。 直到某个用户进入系统, 创建了会话之后, 才会出现ShoppingCart实例。另外, 系统中将会有多个ShoppingCart实例: 每个用户一个。 我们并不想让Spring注入某个固定的ShoppingCart实例到StoreService中。 我们希望的是当StoreService处理购物车功能时, 它所使用的ShoppingCart实例恰好是当前会话所对应的那一个。Spring并不会将实际的ShoppingCart bean注入到StoreService中, Spring会注入一个到ShoppingCart bean的代理, 这个代理会暴露与ShoppingCart相同的方法, 所以StoreService会认为它就是一个购物车。 但是, 当StoreService调用ShoppingCart的方法时, 代理会对其进行懒解析并将调用委托给会话作用域内真正的ShoppingCart bean。

proxyMode作用

proxyMode属性被设置成了ScopedProxyMode.INTERFACES, 这表明这个代理要实
现ShoppingCart接口, 并将调用委托给实现bean。

注意点

如果ShoppingCart是接口而不是类的话, 这是可以的(也是最为理想的代理模式) 。 但如果ShoppingCart是一个具体的类的话, Spring就没有办法创建基于接口的代理了。 此时, 它必须使用CGLib来生成基于类的代理。 所以, 如果bean类型是具体类的话, 我们必须要将proxyMode属性设为ScopedProxyMode.TARGET_CLASS,以此来表明要以生成目标类扩展的方式创建代理。

运行时注入

注入方式

注入方式有:

  • 构造器注入
  • Setter注入

对于强依赖使用构造器注入,而对可选性的依赖使用属性注入

为了避免硬编码问题,Spring提供了两种在运行时求值的方式

  • 属性占位符(Property placeholder)
  • spring表达式语言(spEL)

在Spring装配中, 占位符的形式为使用“${ ... }”包装的属性名称

如果我们依赖于组件扫描和自动装配来创建和初始化应用组件的话, 那么就没有指定占位符的配置文件或类了。 在这种情况下, 我们可以使用@Value注解

public zhangkai{
    @Value("${linkis.ip}") String ip;
    ...
}