Spring 容器初始化过程
2021-11-12 [spring
context
]
bean 容器作为 spring 的核心功能,IoC/DI 都建立在其之上。这篇文章主要讨论 spring 如何初始化容器。
术语解释
- Bean: 在 java 的世界里,每个对象实例又称为 bean。(咖啡豆之于咖啡)
- Context(container): 应用里会有很多对象,spring 都把它们放在一个叫 context 的容器类里,需要的时候调用
context.getBean(beanName)
即可 - IoC(Inverse of Control): 控制反转,直白地说就是开发者只负责实现具体的功能(类的方法),程序启动后,由 spring 负责类的实例化(new 出来)
- DI(Depedency Injection): 依赖注入,spring 将类实例化之后,类之间的组装也交给 spring 完成。
context 初始化流程
下图展示了 ApplicationContext
类在启动时主要的步骤。让我们依次来看下。
解析配置并创建 BeanDefinition
在 spring 发布了第 4 个大版本后,我们有一共有以下 4 种方式来配置应用:
- xml 配置: 即采用最经典的
ClassPathXmlApplicationContext("context.xml")
- 通过配置需要扫描的 package:
AnnotationConfigApplicationContext("package.name")
- 通过
@Configuration
注解指定需要扫描的 class:AnnotationConfigApplicationContext(JavaClass.class)
,和以上两者不同,这种方式通过 java 代码实现。 - groovy 配置:
GenericGroovyApplicationContext("context.groovy")
第 1 步的主要目的是创建所有的 BeanDefinition . BeanDefinition 是一个特殊的接口,通过它你可以获取 bean 的 metadata。 这 4 种类型的配置,每种的解析机制各不相同。
xml 配置
对 xml 配置来说,spring 会使用 XmlBeanDefinitionReader
。它实现了 BeanDefinitionReader
接口。
XmlBeanDefinitionReader
拿到 InputStream
对象,通过 DefaultDocumentLoader
将 xml 文档加载进来。
然后 xml 文档中的每个 bean 元素(<bean />
) 会被解析成 BeanDefinition
,并且将元素的属性 (id, name, class, alias, init-method 等) 赋值到 BeanDefinition
上。
所有的 BeanDefinition
会被放置到一个 Map
中, Map
位于 DefaultListableBeanFactory
类中。代码如下:
/** Map of bean definition objects, keyed by bean name */
privatefinal Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(64);
通过指定扫描的 package 或 @Configuration
的代码配置
这种指定 package 的方式,或者 java 代码的方式和上述 xml 配置大不相同。
这种方式的主要实现类是 AnnotationConfigApplicationContext
new AnnotationConfigApplicationContext(JavaConfig.class);
// or
new AnnotationConfigApplicationContext("package.name");
如果你尝试阅读 AnnotationConfigApplicationContext
代码,会发现有两个属性:
private final ClassPathBeanDefinitionScanner scanner;
private final AnnotatedBeanDefinitionReader reader;
ClassPathBeanDefinitionScanner
负责扫描 package 或者被 @Component 注解的 class。
为了启用 scan 特性,需要如下配置:
// java
@ComponentScan({"package.name"})
// or xml
<context:component-scanbase-package="package.name"/>
AnnotatedBeanDefinitionReader
需要做以下几件事情:
第一是注册所有带 @Configuration
的类以便稍后解析。如果 @Conditional
出现在上面,需要运行其中的表达式,只有结果为 true 的 class 才会被注册。
@Conditional
出现在 spring 4 中,用于在容器初始化时,根据条件配置 bean 和 @Configuration
。
第二是注册一个特殊的 BeanFactoryPostProcessor
,称为 BeanDefinitionRegistryPostProcessor
, 该类使用 ConfigurationClassParser
解析 java 配置,并转化为 BeanDefinition
.
Groovy 配置
这种配置方式和 xml 很相似,除了解析文件从 xml 变为 groovy。
解析工作由类 GroovyBeanDefinitionReader
负责。
配置创建的 BeanDefinition
在经过第一步之后,我们有一个负责存储 BeanDefinition
的 Map。
spring 框架为开发者提供了在 bean 被创建之前干预的机制,换言之,开发者可以获取 class 的 metadata。
获取的方式是实现 BeanFactoryPostProcessor
接口,该接口只有一个方法。
public interface BeanFactoryPostProcessor {
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}
postProcessBeanFactory
方法接受一个 ConfigurableListableBeanFactory
参数,
它包含了许多有用的方法,通过这些方法,我们可以获取所有的 BeanDefinition
的 Names,以及通过指定的 name 获取 BeanDefinition
对象。
String[] getBeanDefinitionNames();
BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;
我们来看一个 spring 实现的 BeanFactoryPostProcessor
的例子。
通常,我们会把数据库连接的配置写在一个 properties 文件里,然后,使用 PropertySourcesPlaceholderConfigurer
加载配置值并注入到对应的字段上。
让我们来仔细看下这里发生了什么。假设我们已经有一个 BeanDefinition
,它的类型是 ClassName
,代码如下:
@Component
public class ClassName {
@Value("${host}")
private String host;
@Value("${user}")
private String user;
@Value("${password}")
private String password;
@Value("${port}")
private Integer port;
}
如果 PropertySourcesPlaceholderConfigurer
没有处理 BeanDefinition
,在创建 ClassName
实例之后,
${host}
的值将会注入到 host 字段上。
如果 PropertySourcesPlaceholderConfigurer
处理之后,其 metadata 将会变成这样:
@Component
public class ClassName {
@Value("127.0.0.1")
private String host;
@Value("root")
private String user;
@Value("root")
private String password;
@Value("27017")
private Integer port;
}
对应的配置值会注入到相应的字段上。
为了 PropertySourcesPlaceholderConfigurer
起作用,还需要进行如下配置:
<context:property-placeholder location="property.properties" />
或者以 java 代码的形式:
@Configuration
@PropertySource("classpath:property.properties")
public class DevConfig{
@Bean
public static PropertySourcesPlaceholderConfigurer configurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
PropertySourcesPlaceholderConfigurer 必须被声明为 static
。
否则,它只会对 @Configuration
里的 @Value
生效。
创建自定义的 FactoryBean
FactoryBean
是用来把创建 bean 过程从 spring 委托给开发者的通用接口。
在过去 bean 的配置只依赖 xml 的日子里,开发者需要一种机制来控制 bean 的创建。
这正是 FactoryBean
干的事情。为了更好地理解,我们来看个 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.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="redColor" scope="prototype" class="java.awt.Color">
<constructor-arg name="r" value="255" />
<constructor-arg name="g" value="0" />
<constructor-arg name="b" value="0" />
</bean>
</beans>
起初一切都很好,但是当你要另一个 Color
的实例时该怎么办呢,再创建一个 bean ?没问题:
<?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.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="redColor" scope="prototype" class="java.awt.Color">
<constructor-arg name="r" value="255" />
<constructor-arg name="g" value="0" />
<constructor-arg name="b" value="0" />
</bean>
<bean id="green" scope="prototype" class="java.awt.Color">
<constructor-arg name="r" value="0" />
<constructor-arg name="g" value="255" />
<constructor-arg name="b" value="0" />
</bean>
</beans>
但是,假如我们需要每次生成一个随机的颜色又该如何?这正是 FactoryBean
发挥作用的地方。
让我们来实现一个 FactoryBean
来负责所有 Color
实例的创建。
package com.malahov.factorybean;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.stereotype.Component;
import java.awt.*;
import java.util.Random;
/**
* User: malahov
* Date: 18.04.14
* Time: 15:59
*/
public class ColorFactory implements FactoryBean<Color> {
@Override
public Color getObject() throws Exception {
Random random = new Random();
Color color = new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255));
return color;
}
@Override
public Class<?> getObjectType() {
return Color.class;
}
@Override
public boolean isSingleton() {
return false;
}
}
再将它加入到 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.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="colorFactory" class="com.malahov.temp.ColorFactory" />
</beans>
现在,创建 Color
的实例将会被委托给 ColorFactory
,由它的 getObject
方法返回创建的 bean。整体流程如下:
如果你采用 java 代码配置 beans,这个接口就没啥用了。
实例化 bean
BeanFactory
负责创建实例化 bean,如果有必要,它将会把创建的过程委托给 FactoryBean
。
实例化 bean 正是基于之前解析出的 BeanDefinition
。
配置已创建的 bean
BeanPostProcessor
接口允许你对创建好的 bean 进行若干设置,该步骤会织入在 bean 加入 container 之前。
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}
两个方法都会对所有的 bean 调用,它们的参数也都一样。 唯一的区别是两个方法调用的顺序。顾名思义,第一个方法在 init 方法前调用,第二个在 init 方法后被调用。
值得一提的是,我们要明白,在调用方法的时候,bean 已经被创建好,而且正在被重新配置。
由两点需要明确:
-
两个方法最后都会返回 bean 实例。如果你返回 null,当你之后从 context 获取该 bean 的时候,你就会得到 null。 因为所有的 bean 都会过
BeanPostProcessor
,在 context 初始化完成后,当你再请求 bean 时,你只会得到 null。 -
如果你希望生成一个 proxy 类(类似 spring 的 aop),记住代理类需要在 init 方法之后生成,换句话说,务必将生成 proxy 的逻辑放在
postProcessAfterInitialization
方法中。
具体的流程如下图所示,调用 BeanPostProcessor
的顺序我们不得而知,但是,我们知道它们是顺序执行。
为了更好的理解,我们来看个例子。
在开发大型项目的时候,开发团队会被分成多个小组。比如负责项目基础设施的公共组件组,还有使用这些组件的业务开发组。 假设业务开发组需要一个功能是生成随机数,并注入到 bean 的字段中。
首先,我们编写一个注解,用于标记这些待注入的字段。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectRandomInt {
int min() default 0;
int max() default 10;
}
默认情况下,随机数的范围时 0 到 10.
然后,我们编写该注解的处理逻辑,即一个负责处理该注解的 BeanPostProcessor
实现类。
@Component
public class InjectRandomIntBeanPostProcessor implements BeanPostProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(InjectRandomIntBeanPostProcessor.class);
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
LOGGER.info("postProcessBeforeInitialization::beanName = {}, beanClass = {}", beanName, bean.getClass().getSimpleName());
Field[] fields = bean.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(InjectRandomInt.class)) {
field.setAccessible(true);
InjectRandomInt annotation = field.getAnnotation(InjectRandomInt.class);
ReflectionUtils.setField(field, bean, getRandomIntInRange(annotation.min(), annotation.max()));
}
}
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
private int getRandomIntInRange(int min, int max) {
return min + (int)(Math.random() * ((max - min) + 1));
}
}
BeanPostProcessor
的代码非常简单,但需要注意的是 BeanPostProcessor
必须先实例化。要么通过 @Component
注解,要么在 xml 中声明。
公共组件组完成了任务,该轮到业务开发组来使用它了:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MyBean {
@InjectRandomInt
private int value1;
@InjectRandomInt(min = 100, max = 200)
private int value2;
private int value3;
@Override public String toString() {
return "MyBean{" +
"value1=" + value1 +
", value2=" + value2 +
", value3=" + value3 +
'}';
}
}
结果,所有从 context 中获取的 MyBean
实例,将会拥有已经被初始化的 value1 和 value2 字段。
还值得注意的是,将值注入这些字段的阶段取决于你的 bean 具有什么样的 @Scope
。
- SCOPE_SINGLETON - 初始化将会在 context 初始化时发生
- SCOPE_PROTOTYPE - 初始化将会在每次从 context 请求 bean 的时候发生
在第二种情况下,你的 bean 由于会经过所有的 BeanPostProcessor
,可能会对程序性能有显著影响。