快递100 是专业的快递物流互联网平台,为数以亿计的用户提供快递物流的查、寄件服务。快递100 的各种服务已经稳定高效的运行了多年,为中国的快递物流行业的快速发展做出了很大的贡献,这一切都离不开背后默默贡献的程序猿们。
从第一个独立站点上线至今,快递100 已经走过了10个年头,10年沧海桑田,10年经历了一次又一次的技术、架构升级。近年来,由于spring-boot、spring-cloud技术的兴起,快递100 技术团队也开始对后端服务进行重构,基于spring-boot、spring-cloud技术,对原有的系统进行微服务化改造。在改造的前期,由于许多开发人员对spring系列的技术缺少系统性的认知,所以我们编辑整理了一个spring系列技术使用的手册,帮助开发人员可以快速的上手。
目录
2.4 AnnotationConfigApplicationContext
3.5.2 消除歧义(@Primary注解和@Qualifier注解)
1. Spring IoC简介
IoC是Inversion of Control的简称,中文意思为控制反转,Ioc理念是Spring框架的两个核心理念之一(另外一个是面向切面:Aspect Oriented Programing,AOP),Spring框架本质上是一个IoC容器编程的框架。
IoC的理念本身并不是Spring,甚至不是java语言的首创。对于传统的java编程方式,我们通常通过new关键字来创建对象,比如类A依赖于类B,我们会在类A中通过:B b = new B()这样的代码来创建一个类B的实例,如果类B是个接口或者抽象类,假如其有一个实现类C,则类A中创建B的实例的代码变为:B b = new C(),这导致了类A和类B的实现类C之间的耦合,即使实际上对于类A来说,可能并不关心B的具体实现方式。而基于IoC的编程则不是这样,IoC是通过描述来生成或者获取对象的技术,当类A依赖于类B时,只需要通过配置文件、注解等方式对依赖需求进行描述,就可以基于这个描述获取到一个类B的实例,类A无需关心类B的具体实现方式,也就解除了和类B具体实现类之间的耦合。
IoC技术基于描述来生成或者获取对象,这些对象都有Spring框架进行管理。此外,这些对象之间并不是孤立存在的,它们之间可能存在着各种依赖关系,如上面的例子中,类A的实例依赖于类B的实例,因此在创建类A的实例时,还需要将类B的实例设置到类A的属性中,这就是依赖注入,在Spring应用中,依赖注入的功能也是由框架提供的。
2. IoC容器
2.1 Spring IoC容器
如前面所述,Spring框架基于IoC理念提供了通过描述来创建或者获取对象的功能和依赖注入的功能,这些功能由IoC容器提供,而由容器创建的对象通常称为Bean,IoC容器实际上负责Bean的创建、销毁、管理、注入和它们之间的关系的维护。
Spring IoC容器的定义由一系列的接口组成,其顶级接口为BeanFactory。Spring IoC容器接口的类图如下:
2.2 BeanFactory接口
BeanFactory是Spring IoC容器的顶级接口,定义了IoC容器的主要方法。这些方法如下:
- getBean方法:用于从IoC容器中获取一个Bean,在BeanFactory中定义了多个getBean方法的重载,可以通过Bean的名称、类型等多种方式从容器中获取到指定的Bean
- containsBean方法:判断容器中是否包含指定名称的Bean,如果是则返回true,否则返回false
- isSingleton方法:判断指定名称的Bean的作用域是否为singleton(单例)
- isPrototype方法:判断指定名称的Bean的作用域是否为prototype(原型)
- isTypeMatch方法:判断指定名称的Bean的类型是否匹配指定的类型,此方法包含两个重载方法,分别通过ResolvableType和Class类型的参数指定匹配的类型
- getType方法:获取指定名称的Bean的类型
- getAlias方法:获取指定名称的Bean定义的别名数组
2.3 ApplicationContext接口
BeanFactory接口中定义了IoC容器的主要方法,但基本上都是集中在获取Bean或者Bean的一些基本属性方面,总体来说功能还是比较弱的,于是在BeanFactory接口的基础上,Spring框架提供了更高级、包含更多功能的IoC容器接口:ApplicationContext。
ApplicationContext接口通过继承上级接口,进而继承了BeanFactory中定义的各个方法,并在此基础上,扩展了更多的接口,因此具有更强大的功能。BeanFactory接口和ApplicationContext接口是Spring IoC容器最重要的接口设计,因为ApplicationContext接口具有更多的功能,因此我们实际上使用的IoC容器,大多数都是ApplicationContext接口的实现类。
在BeanFactory接口的基础上,ApplicationContext接口还扩展了如下的接口:
- MessageSource:提供消息国际化功能
- EnvironmentCapable:环境参数可配置化功能
- ApplicationEventPublisher:应用事件发布功能
- ResourcePatternResolver:资源模式解析功能
2.4 AnnotationConfigApplicationContext
AnnotationConfigApplicationContext是ApplicationContext接口的实现类,是基于注解的描述来装配和注入Bean的IoC容器实现。Spring IoC同时支持以Xml和注解的方式来描述Bean的创建、销毁、管理和依赖关系等,但对于Spring boot来说,建议的方式是以注解的方式来描述,因此spring boot中Bean的装配和注入的方式,实质上与AnnotationConfigApplicationContext是一致的。
AnnotationConfigApplicationContext能够根据@Configuration和@Bean等注解来实现Bean的装配和注入。
3. 装配Bean
3.1 扫描装配Bean
通过@Component和@ComponentScan注解配合,可以通过扫描类的方式,将类装配到IoC容器中。@Component注解用于标注类是否需要进行装配,而@ComponentScan注解则标注扫描装配Bean的策略。
在spring boot应用中,@ComponentScan注解可以标注在启动类或者带有@Configuration注解的java配置类中,指定应用扫描装配Bean的策略。@ComponentScan注解中包含basePackages配置,可以指定扫描装配Bean的基础包路径,还包含其他的一些配置,让Spring框架按照配置的策略对包含@Component注解(或其他类似@Service、@Controller等标注了@Component注解的注解)的类进行扫描和装配。关于@ComponentScan注解的详细情况,请参见后面@ComponentScan注解的部分。
3.2 装配第三方Bean
应用中可能会引用一些第三方的类库,这时候可能希望将第三方的类创建的对象也装配到Spring IoC容器中,这种情况下由于不能给类加上@Component注解,所以没有办法通过扫描的方式来装配Bean,对此,我们可以采用@Bean注解标注的方式,进行Bean的装配。
如下图所示的代码中,在@Configuration注解标注的DaoConfiguration类中,定义了一个createExecutorInterceptor方法,该方法返回一个UpdateExecutorInterceptor对象,并标注了@Bean注解。Spring框架在初始化时,会调用这个方法,得到其返回的UpdateExecutorInterceptor对象,并将其装配到IoC容器中,转配的Bean的名称为@Bean注解的value值“executorInterceptor”。关于@Bean注解详细的描述,请参见@Bean注解部分的内容。
@Configuration
public class DaoConfiguration {
@Bean("executorInterceptor")
public UpdateExecutorInterceptor createUpdateExecutorInterceptor() {
return new UpdateExecutorInterceptor();
}
}
3.3 根据条件装配Bean
有些情况下,可能存在一些客观因素导致一些Bean无法完成初始化,这时候框架可能会抛出异常导致系统不能完成启动。如在配置数据库连接时,可能有些参数配置不完整导致连接数据库报错,这时候如果框架继续装配和初始化数据源Bean,就可能会抛出异常导致应用不能启动,而我们可能希望在这种情况下框架可以不去装配和初始化数据源Bean,让应用仍然能够正常启动,这个时候就可以根据条件来进行Bean的装配。
Spring框架提供了@Conditional注解来标注Bean装配时需要的条件。@Conditional注解的value配置需要指定一个Condition接口的实现类,用于实现Bean的装配条件判断。Condition接口包含一个matches方法,框架会根据这个方法的返回值来判断是否要进行Bean的装配,如果返回true则进行Bean的装配,否则不进行装配。
下面的代码片段描述了如何通过@Conditional注解来标注Bean的装配条件。
@Bean
@Conditional(TestCondition.class)
public UpdateExecutorInterceptor createUpdateExecutorInterceptor() {
return new UpdateExecutorInterceptor();
}
public class TestCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext,
AnnotationTypeMetadata annotationTypeMetadata) {
return true;
}
}
Spring boot框架基于@Conditional注解和Condition接口实现了一系列的条件装配注解,如@ConditionalOnMissingBean等,这些注解均已实现按照特定的条件判断是否进行Bean的装配,可以直接用其进行标注而无需自行实现Condition接口。关于spring boot框架实现的系列条件装配注解的详细描述,请参见后面条件注解部分的内容。
3.4 延迟加载
应用中可以通过在@ComponentScan注解中配置lazyInit为true(默认为false),或者在Bean描述处(使用@Component注解标注的类或者@Bean注解标注的方法中)使用@Lazy注解标注,来告知框架对应的Bean需要采用延迟加载策略。表描述为延迟加载的Bean不会在装配完成之后进行初始化,而只会在该Bean被第一次使用时才进行初始化。
下面两个代码片段,分别是通过@ComponentScan注解和@Lazy注解配置延迟加载策略的示例。
@Configuration
@ComponentScan(lazyInit = true)
public class AppConfig {
@Bean
public Person createPerson {
return new Person();
}
}
@Component
@Lazy
public class Car implements Vehicle {
}
3.5 Bean的注入
3.5.1 注入Bean(@Autowired注解)
应用中可以在类中定义依赖Bean的属性,然后通过标注@Autowired注解来注入依赖的Bean。如下面的代码所示:
@Component
public class Person {
@Autowired
private Vehicle vehicle;
}
3.5.2 消除歧义(@Primary注解和@Qualifier注解)
如上面的代码所示,Person类中依赖Vehicle接口实例,通过标注@Autowired注解,让框架将实现了Vehicle接口的Bean注入,当Vehicle接口只有一个实现类的Bean被IoC装配时没有问题,但如果Vehicle接口有多个实现类的Bean被IoC容器装配,则由于容器中存在多个Vehicle接口类型的Bean,框架无法判断Person类中需要注入哪一个Bean,抛出异常。
假如Vehicle接口有两个实现类,分别为Car和Bicycle,这两个实现类的Bean均转配到IoC容器中,现在需要在Person类中注入Car类型的Bean,有如下三种方式可以消除歧义:
- 修改Person类中Vehicle类型属性的名称,将其修改成为car,这样,由于Car类Bean装配时默认的名称为car,框架会自动进行判断,将其注入到Person的car属性中
@Component
public class Person {
@Autowired
private Vehicle car;
}
- 使用@Primary注解标注Car类,表示此类的Bean对于Vehicle接口类型的Bean注入来说具有优先级,框架会优先将Car类的Bean注入到Person类或者其他包含Vehicle类型属性的Bean中。但如果Car类和Bicycle类均标注了@Primary注解,则还是无法消除歧义,导致框架报错
@Component
@Primary
public class Car implements Vehicle {
}
@Component
public class Person {
@Autowired
private Vehicle vehicle;
}
- 使用@Qualifier注解,通过注解中value项配置指定需要注入的bean的名称。如下面的代码所示,通过@Qualifier注解指定注入名称为car的Bean,框架会在IoC容器中,按照类型为Vehicle和名称为car作为条件去查找Bean进行注入,因此可以消除歧义
@Component
public class Person {
@Autowired
@Qualifier("car")
private Vehicle vehicle;
}
3.5.3 带参数的构造方法注入
对于构造方法包含参数的Bean的依赖注入,可以使用@Autowired注解标注构造方法参数进行注入,而且同样可以使用@Primary、@Qualifier注解来消除歧义。
@Component
@Lazy
public class Person {
private Vehicle vehicle;
public Person(@Autowired @Qualifier("car") Vehicle vehicle) {
this.vehicle = vehicle;
}
}
3.6 Bean的生命周期
3.6.1 Bean的生命周期介绍
Spring IoC容器中,Bean的生命周期大体可以分为四个阶段:
- Bean定义阶段:进行资源定位(如@ComponentScan注解扫描指定的包中包含@Component注解的类)和解析,根据解析的结果创建Bean的定义,将其封装成BeanDefinition对象,发布到IoC容器
- Bean初始化阶段:IoC容器创建Bean的实例,完成各种初始化操作,根据Bean的依赖关系,完成Bean的注入(根据@Autowired注解或者xml的配置)
- Bean的生存期:应用从容器中获取Bean完成各种业务操作
- Bean销毁阶段:完成Bean的销毁操作
Bean的整个生命周期涉及到多个环节,这些环节之间的关系,如下图所示:
3.6.2 应用介入Bean的生命周期
Spring框架提供了一系列接口、注解和配置方法等,使应用可以介入到Bean的生命周期的各个环节,对Bean的装配、初始化、销毁等等环节施加影响。应用可以通过实现接口、使用注解标注、通过Xml配置等方式,介入到Bean的生命周期,具体的说明如下:
- BeanNameAware接口:包含setBeanName方法定义,在容器初始化Bean设置Bean的名称时,调用这个方法。应用中可以在装配的类中实现这个接口,获取到Bean装配到容器中的名称
- BeanFactoryAware接口:包含setBeanFactory方法,装配的类中可以实现这个接口,在Bean初始化之后容器会调用其setBeanFactory方法,可以获取到装配此Bean的BeanFactory实例
- @PostContruct注解:用于标注方法,在容器完成对Bean的初始化之后,会调用Bean中标记了@PostConstruct注解的方法,应用可以在此方法中完成自己的初始化操作
- BeanPostProcessor接口:包含postProcessBeforeInitialization和postProcessAfterInitialization方法,如果应用实现类此接口,并且装配到IoC容器中,容器会在每个Bean进行预初始化时调用实现类Bean的postProcessBeforeInitialization方法,在初始化完成后调用其postProcessAfterInitialization方法
- ApplicationContextAware接口:包含setApplicationContext方法,装配的类中如果实现了此接口,容器会调用setApplicationContext方法,将实现了ApplicationContext接口的容器实例传递给Bean。此接口只有在IoC容器是ApplicationContext接口的实例时才有效
- InitializingBean接口:包含afterPropertiesSet方法,装配的类中如果实现了此接口,容器会在完成Bean的初始化后,调用其afterPropertiesSet
- @PreDestroy注解:在容器销毁Bean之前,会调用Bean的使用此注解标注的方法,供应用进行一些Bean销毁前的预处理操作,如释放资源等
- DisposeBean接口:包含destroy方法,如果Bean实现了此接口,容器会在需要销毁Bean时,调用其destroy方法来销毁Bean
3.6.3 BeanFactoryProcessor接口
BeanFactory后置处理并不是Bean生命周期的一部分,但是因为BeanFactory后置处理跟Bean的定义、初始化、装配、注入等往往息息相关,因此也将其放在Bean生命周期部分一并讲述。
应用中可以实现BeanFactoryProcessor接口,并使用@Component注解进行标注,框架初始化时会扫描这个类,并在BeanFactory完成初始化之后,调用其postProcessBeanFactory方法,该方法会给应用传递一个ConfigurableListableBeanFactory实例,这是一个可配置的BeanFactory实现类实例,应用可以根据具体的需求,增加或修改BeanFactory中的Bean定义。
BeanFactoryProcessor有一个子接口:BeanDefinitionRegistryPostProcessor,该接口扩展了BeanFactoryProcessor接口,增加了一个postProcessBeanDefinitionRegistry方法,在处理BeanFactoryProcessor接口的实现类时,如果框架发现该实现类是BeanDefinitionRegistryPostProcessor接口的实现类,则会在调用postProcessBeanFactory方法前调用postProcessBeanDefinitionRegistry方法,将一个BeanDefinitionRegistry对象传递给BeanDefinitionRegistryPostProcessor接口实现类,应用中可以据此来给IoC容器注册更多的Bean定义。
BeanFactoryProcessor接口(包括BeanDefinitionRegistryPostProcessor接口)实现类的处理,是在BeanFactory完成初始化以及Bean定义(BeanDefinition对象发布)之后进行的,在这个时候,Bean的定义已经注册,但容器还未对Bean进行初始化,因此应用可以通过实现BeanFactoryProcessor接口或者BeanDefinitionRegistryPostProcessor接口,对已注册的Bean进行一些修改,或者注册更多的Bean,这对于应用来说,可能是具有非常重要的意义的。比如说应用可以在这个地方将一些原本需要按照应用系统的配置规则来生成的对象,通过创建BeanDefinition的方式,注册到Spring IoC容器中,接入到整个框架中。
3.6.4 Bean的作用域
作用域定义Bean的影响范围,Spring IoC中Bean有多种作用域类型,这些作用域及其说明如下:
- singleton:在所有spring应用中适用,表示Bean在容器中以单例的形式存在,只会创建一个实例
- prototype:所有spring应用使用,表示从IoC容器中获取Bean时,均会创建一个新的实例
- session:spring web应用适用,表示在http会话周期内,容器中只存在一个Bean的实例
- application:spring web应用适用,表示在web应用的整个生命周期内,容器中只存在一个Bean的实例
- request:spring web应用适用,表示在一次http请求周期内,容器中只存在一个Bean的实例
- globalSession:spring web应用适用,表示在一个全局的http会话周期内,一个Bean定义在容器中只会存在一个实例
Bean的作用域,可以使用@Scope注解,搭配@Component注解或@Bean注解使用,标注着两个注解定义的Bean的作用域。
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class Car implements Vehicle {
}
@Configuration
@ComponentScan(lazyInit = true)
public class AppConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Person createPerson() {
return new Person();
}
}
3.6.5 装配Xml配置的Bean
虽然spring boot建议使用注解来配置Bean,但同样也支持基于Xml配置文件配置的Bean,通过@ImportSource注解,可以指定Xml配置文件的路径,框架会解析和装配Xml配置文件中包含的Bean。
@Configuration
@ComponentScan(lazyInit = true, basePackages = "springboot.demo")
@ImportResource("classpath:spring-bean.xml")
public class AppConfig {
}