我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。
1.缘起
在看一段基于 spring security 的鉴权代码的时候,我发现一个有趣的 Bean 声明和方法调用。在一个 @Configuration 注解的配置类中用 @Bean 注解了一个方法 tokenStore,声明了 Spring bean: tokenStore。在同一个配置类另一个方法 configure 中调用了这个被 @Bean 注解的方法 tokenStore 来获取 TokenStore 实例。
那么这会导致系统中存在多个 TokenStore 实例吗?如果是两个实例,则一个应该是 Spring bean 实例,一个是 configure 方法中通过 tokenStroe() 方法创建的实例。代码如下:
@Configuration
public class AuthorizationConfig {
@Bean
public TokenStore tokenStore() {
RedisTokenStore redis = new RedisTokenStore(connectionFactory);
return redis;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
/*使用oauth2的密码模式时需要配置authenticationManager*/
endpoints.authenticationManager(authenticationManager);
endpoints.tokenStore(tokenStore())
...
}
...
}
答案是否定的,在系统中只有一个实例,这个实例就是 spring bean。这是怎么做到的呢?继续阅读之前,可以闭上眼睛思考一下。
2.原理
通过调式代码,我们可以发现这个配置类已经被 CGLIB 代理了,这个配置类的实例类名变成了 AuthorizationConfig$$EnhancerBySpringCGLIB
,如图:
断点堆栈如图: 这个堆栈图从最底下向上看,配置类的 configure 方法调用本类方法 tokenStore,变成了调用
AuthorizationConfig$$EnhancerBySpringCGLIB
的tokenStore了,而这个调用被 ConfigurationClassEnhancer$$BeanMethodInterceptor 拦截器拦截,接着堆栈出现了我们熟悉的 Spring getBean 的调用堆栈(当 bean 不存在的时候,就会触发创建 bean)。
由此可见spring 容器通过 CGLIB 代理了配置类,调用配置类 @Bean 注解的方法时,这个方法会被拦截。拦截器会通过 Spring 容器的机制去获取这个方法上 @Bean 注解声明的 bean,如果这个bean 实例还不存在,Spring 容器会创建 bean 实例,而这个 bean 是通过配置类的 tokenStore 方法创建的,所以最终找到通过代理类调用到了配置类的 tokenStore 方法创建了 bean 实例。
3.机制探究
Spring 为 @Configuration 专门设计了一个 BeanFactoryPostProcessor 实现类ConfigurationClassPostProcessor,我们知道 Spring 在初始化加载 bean 的过程中,预留了 BeanFactoryPostProcessor 扩展点
。这个扩展点的执行时机是在 BeanFactory 初始化之后,所有的Bean定义已经被加载,但Bean的实例还没被创建(不包括 BeanFactoryPostProcessor 实例)的时候。Spring 会在这个时候调用 BeanFactoryPostProcessor 的 postProcessBeanFactory 方法。这个扩展点通常用于修改 bean 的定义,bean 的属性值等。
我们来看看 ConfigurationClassPostProcessor 的 postProcessBeanFactory 方法实现(省略了很多代码,只列出关键部分,可以参考 spring 5.2 版本的源代码:
/**
* Prepare the Configuration classes for servicing bean requests at runtime
* by replacing them with CGLIB-enhanced subclasses.
* 通过 CGLIB 增强的子类来代替配置类来为 bean 请求提供支持
*/
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
int factoryId = System.identityHashCode(beanFactory);
if (this.factoriesPostProcessed.contains(factoryId)) {
throw new IllegalStateException(
"postProcessBeanFactory already called on this post-processor against " + beanFactory);
}
this.factoriesPostProcessed.add(factoryId);
if (!this.registriesPostProcessed.contains(factoryId)) {
// BeanDefinitionRegistryPostProcessor hook apparently not supported...
// Simply call processConfigurationClasses lazily at this point then.
processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);
}
//配置类主要的增强逻辑
enhanceConfigurationClasses(beanFactory);
beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory));
}
public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<>();
for (String beanName : beanFactory.getBeanDefinitionNames()) {
...
//如果配置类是 full 模式,则将配置类加入到需要增强的配置类列表中
if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) {
...
configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef);
}
}
if (configBeanDefs.isEmpty()) {
// nothing to enhance -> return immediately
return;
}
//遍历需要增强的配置类列表,为每个配置类实现增强逻辑
ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();
for (Map.Entry<String, AbstractBeanDefinition> entry : configBeanDefs.entrySet()) {
...
// 获取配置类的 Class 对象
Class<?> configClass = beanDef.getBeanClass();
// 增强实现
Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);
if (configClass != enhancedClass) {
...
//将增强后的配置类设置到 bean 定义对象中
beanDef.setBeanClass(enhancedClass);
}
}
}
从上面关键代码,我们可以看出 Spring 会为符合条件的 full 模式的配置类实施增强。
注: Full 模式和 lite 模式
Spring 把 bean 分成两类:full 模式和 lite 模式。在 @Configuration 注解的配置类中声明的 bean 就是 full 模式的,其他的 spring bean,比如在 @Component 注解的类中声明的 bean 都是 lite 模式。也就是说通常只有 @Configuration 注解的配置类需要增强,这也是 @Configuration 注解和其他类型的组件注解的一个重要的区别。
下面我们接着探索 ConfigurationClassEnhancer 是如何实现增强的,关键代码如下:
public Class<?> enhance(Class<?> configClass, @Nullable ClassLoader classLoader) {
if (EnhancedConfiguration.class.isAssignableFrom(configClass)) {
...
//说明已经增强过了,直接返回
return configClass;
}
Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
if (logger.isTraceEnabled()) {
logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s",
configClass.getName(), enhancedClass.getName()));
}
return enhancedClass;
}
/**
* Creates a new CGLIB {@link Enhancer} instance
*/
private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
Enhancer enhancer = new Enhancer();
//将配置类设置为增强结果类的父类
enhancer.setSuperclass(configSuperClass);
enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
enhancer.setUseFactory(false);
//设置增加类的命名策略,即增加 BySpringCGLIB,可以看前面调试贴图中的类名
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader));
//设置回调(拦截器)过滤器,就是说当配置类方法被调用的时候,会先执行符合过滤器条件的拦截器逻辑
enhancer.setCallbackFilter(CALLBACK_FILTER);
enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
return enhancer;
}
/**
* Uses enhancer to generate a subclass of superclass,
* ensuring that callbacks are registered for the new subclass.
*/
private Class<?> createClass(Enhancer enhancer) {
Class<?> subclass = enhancer.createClass();
// 注册拦截器
Enhancer.registerStaticCallbacks(subclass, CALLBACKS);
return subclass;
}
拦截器分析
增加逻辑主要是通过CGLIB enhancer 以配置类为父类创建一个代理子类,并设置了调用配置方法的时候,需要执行的拦截器。下面我们看看拦截器。
class ConfigurationClassEnhancer {
// The callbacks to use. Note that these callbacks must be stateless.
private static final Callback[] CALLBACKS = new Callback[] {
new BeanMethodInterceptor(),
new BeanFactoryAwareMethodInterceptor(),
NoOp.INSTANCE
};
这里面和我们主题相关的就是 BeanMethodInterceptor。这个拦截器的主要逻辑就是拦截对于 @Bean 注解方法的调用,并看声明的 Spring bean 是否已经存在,如果存在则直接返回容器中的 Spring bean。否则真正的配置类的方法创建 Spring bean 实例。
总结
Spring 利用 BeanFactoryPostProcessor 扩展点, 通过 CGLIB enhancer 增强了 @Configuration 注解的配置类。重载的方式是创建了一个新的以配置类为父类增强子类。对于配置类中 @Bean 注解的方法的调用将会被拦截器拦截。拦截器的逻辑是判断声明的 Spring bean 在容器中是否已经存在,如果存在则直接返回容器中的 Spring bean。否则真正的配置类的方法创建 Spring bean 实例。