公司的系统在引入Spring-Session以实现Session共享之后,自然而然出现了动态启用的需求,毕竟在开发时我们并不需要开启这个功能。
1. 目标
以配置文件的形式,即通过一个配置项切换spring-session的启用和停止。
2. 分析
我们回顾下启用spring-session的必要条件:
1. 名为springSessionRepositoryFilter
的DelegatingFilterProxy
注册到Servlet容器中。
2. 构造上面所代理的实际干活的Filter(spring-session-xx.jar中被@Configuration
修饰的SpringHttpSessionConfiguration
类构造的那个)时所必须的bean, 例如redis相关的JedisConnectionFactory
等。
3. 解决方案
最终解决方案是
1. 使用Servlet3.0提供的ServletContainerInitializer
接口功能来动态注册Filter
。
2. 使用Spring动态注入Bean的方式将Spring-Session必要的几个Bean注入到spring.xml
生成的WebApplicationContext
中
4. 细节
4.1 动态注册DelegatingFilterProxy
参见本人的博客Spring-Session源码研究之Start_Servlet3.0 中的第1.3小节。因为注册这个Filter的逻辑其实Spring-Session都已经帮我们准备好了,我们所要做的只是进行一下简单的配置判断即可。为了方便,我这里还是贴一下相关代码。
```
public class SpringSessionInitializer extends AbstractHttpSessionApplicationInitializer {
public void onStartup(ServletContext servletContext) throws ServletException {
// Servlet3.0+容器会回调本方法
// 我们需要自定义决定是否载入这个Filter
String isSingleUserLogin = (String) PropertiesUtil.getProperty("login.singleUserLogin");
if (!("true".equalsIgnoreCase(isSingleUserLogin))) {
return;
}
// 交给基类去注入Filter
super.onStartup(servletContext);
}
}
```
4.2 动态注册所必须的Bean
我们最终选择了通过实现BeanDefinitionRegistryPostProcessor
接口来达到动态注册的目的。
这里有一点要注意的是解析@Configuration
的ConfigurationClassPostProcessor
, 也是实现了 BeanDefinitionRegistryPostProcessor
接口。所以我们需要安排这两个类执行的先后优先级。
我们注意到ConfigurationClassPostProcessor
实现了PriorityOrdered
这个标志性接口,其实现的getOrdered
方法的返回值显示是最低优先级, 所以我们只需要比这个优先级稍微高一点就可以了。
// 通过实现BeanDefinitionRegistryPostProcessor接口来进行动态注册bean
// 实现PriorityOrdered接口将自己的被调用时机至于ConfigurationClassPostProcessor类的被调用时机前面;
// 这样就可以在Spring框架解析 RedisHttpSessionConfiguration 配置类之前将其该配置内所需要的Bean(也就是本SingleUserBeanDefinitionRegistryPostProcessor类内部动态注册的Bean)注册进容器;
// 这样在解析RedisHttpSessionConfiguration 配置类时就不会报告NoSuchBeanDefinitionException了.
public class SingleUserBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry bdRegistry) throws BeansException {
String isSingleUserLogin = (String) PropertiesUtil.getProperty("login.singleUserLogin");
if (!("true".equalsIgnoreCase(isSingleUserLogin))) {
return;
}
// ------------------------- httpSessionStrategy
// 最终会注入到SpringHttpSessionConfiguration中
BeanDefinitionBuilder bdBuilder = BeanDefinitionBuilder
.genericBeanDefinition(CookieHttpSessionStrategyDecorator.class);
//注册
bdRegistry.registerBeanDefinition("httpSessionStrategy", bdBuilder.getRawBeanDefinition());
// ------------------------- jedisConnectionFactory
bdBuilder = BeanDefinitionBuilder
.genericBeanDefinition(org.springframework.data.redis.connection.jedis.JedisConnectionFactory.class);
//直接注入spel表达式; 注意这里是可以解析到的.
// 具体的源码逻辑参见 AbstractBeanFactory 类的 evaluateBeanDefinitionString 方法
// 如果想看点现成的, 可以参见《Spring源码深度解析》 P139
//设置依赖
bdBuilder.addPropertyValue("hostName", "${db.redis.hostName}");
bdBuilder.addPropertyValue("port", "${db.redis.port}");
//注册
bdRegistry.registerBeanDefinition("redisConnectionFactory", bdBuilder.getRawBeanDefinition());
// ------------------------- redisHttpSessionConfiguration
//构造bean定义
bdBuilder = BeanDefinitionBuilder.genericBeanDefinition(RedisHttpSessionConfiguration.class);
//设置依赖
bdBuilder.addPropertyValue("maxInactiveIntervalInSeconds",
"${db.redis.maxInactiveIntervalInSeconds}");
//注册
bdRegistry.registerBeanDefinition("redisHttpSessionConfiguration", bdBuilder
.getRawBeanDefinition());
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory arg0) throws BeansException {
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE + 1; // 比ConfigurationClassPostProcessor 级别高一些
}
}
5. 后记
- 当想到需要动态注册时,本人的第一反应就是
BeanFactoryPostProcessor
; - 然后在网上找了下, 发现还可以使用
ImportBeanDefinitionRegistrar
和BeanDefinitionRegistryPostProcessor
。后面这两看名字大概就能猜到做啥用的,所以Spring里的命名还是相当不错的! - 关于上面这两个接口可以直接看下面的博文。