https://blog.csdn.net/patrickyoung6625/article/details/45694157
1.共享Session问题
HttpSession是通过Servlet容器创建和管理的,像Tomcat/Jetty都是保存在内存中的。而如果我们把web服务器搭建成分布式的集群,然后利用LVS或Nginx做负载均衡,那么来自同一用户的Http请求将有可能被分发到两个不同的web站点中去。那么问题就来了,如何保证不同的web站点能够共享同一份session数据呢?
2.Spring Session介绍
Spring Session是Spring的项目之一,GitHub地址:https://github.com/spring-projects/spring-session。
Spring Session提供了一套创建和管理Servlet HttpSession的方案。Spring Session提供了集群Session(Clustered Sessions)功能,默认采用外置的Redis来存储Session数据,以此来解决Session共享的问题。
下面是来自官网的特性介绍:
Features
Spring Session provides the following features:
- API and implementations for managing a user's session
- HttpSession - allows replacing the HttpSession in an application container (i.e. Tomcat) neutral way
- Clustered Sessions - Spring Session makes it trivial to support clustered sessions without being tied to an application container specific solution.
- Multiple Browser Sessions - Spring Session supports managing multiple users' sessions in a single browser instance (i.e. multiple authenticated accounts similar to Google).
- RESTful APIs - Spring Session allows providing session ids in headers to work with RESTful APIs
- WebSocket - provides the ability to keep the HttpSession alive when receiving WebSocket messages
3.集成Spring Session的正确姿势
下面是实际调试通过的例子,包含下面4个步骤:
(1)第一步,添加Maven依赖
根据官网Quick Start展示的依赖,在项目pom.xml中添加后各种找不到类引用。于是查看Spring Session项目的build.gradle文件,居然没有配置依赖的项目,难道还要我自己去找它的依赖,太不专业了吧?!!!
- <dependencies>
- <dependency>
- <groupId>org.springframework.session</groupId>
- <artifactId>spring-session</artifactId>
- <version>1.0.1.RELEASE</version>
- </dependency>
- </dependencies>
终于在多番仔细研究Spring Session项目源码之后,看到了spring-session-data-redis项目:
build.gradle文件里配置了Spring Session编译依赖的3个项目:
- apply from: JAVA_GRADLE
- apply from: MAVEN_GRADLE
- apply plugin: 'spring-io'
- description = "Aggregator for Spring Session and Spring Data Redis"
- dependencies {
- compile project(':spring-session'),
- "org.springframework.data:spring-data-redis:$springDataRedisVersion",
- "redis.clients:jedis:$jedisVersion",
- "org.apache.commons:commons-pool2:$commonsPoolVersion"
- springIoVersions "io.spring.platform:platform-versions:${springIoVersion}@properties"
- }
于是,真正的Maven依赖改成spring-session-data-redis就OK了:
- <dependency>
- <groupId>org.springframework.session</groupId>
- <artifactId>spring-session-data-redis</artifactId>
- <version>1.0.1.RELEASE</version>
- </dependency>
(2)第二步,编写一个配置类,用来启用RedisHttpSession功能,并向Spring容器中注册一个RedisConnectionFactory。
- import org.springframework.context.annotation.Bean;
- import org.springframework.data.redis.connection.RedisConnectionFactory;
- import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
- import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
- @EnableRedisHttpSession(maxInactiveIntervalInSeconds = 7200)
- public class RedisHttpSessionConfig {
- @Bean
- public RedisConnectionFactory connectionFactory() {
- JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
- connectionFactory.setPort(6379);
- connectionFactory.setHostName("10.18.15.190");
- return connectionFactory;
- }
- }
(3)第三步,将RedisHttpSessionConfig加入到WebInitializer#getRootConfigClasses()中,让Spring容器加载RedisHttpSessionConfig类。WebInitializer是一个自定义的AbstractAnnotationConfigDispatcherServletInitializer实现类,该类会在Servlet启动时加载(当然也可以采用别的加载方法,比如采用扫描@Configuration注解类的方式等等)。
- //该类采用Java Configuration,来代替web.xml
- public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
- @Override
- protected Class<?>[] getRootConfigClasses() {
- return new Class[]{Config1.class, Config2.class, RedisHttpSessionConfig.class};
- }
- //......
- }
(4)第四步,编写一个一个AbstractHttpSessionApplicationInitializer实现类,用于向Servlet容器中添加springSessionRepositoryFilter。
- import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
- public class SpringSessionInitializer extends AbstractHttpSessionApplicationInitializer {
- }
4. Spring Session原理
(1)前面集成spring-sesion的第二步中,编写了一个配置类RedisHttpSessionConfig,它包含注解@EnableRedisHttpSession,并通过@Bean注解注册了一个RedisConnectionFactory到Spring容器中。
而@EnableRedisHttpSession注解通过Import,引入了RedisHttpSessionConfiguration配置类。该配置类通过@Bean注解,向Spring容器中注册了一个SessionRepositoryFilter(SessionRepositoryFilter的依赖关系:SessionRepositoryFilter --> SessionRepository --> RedisTemplate --> RedisConnectionFactory)。
- package org.springframework.session.data.redis.config.annotation.web.http;
- @Configuration
- @EnableScheduling
- public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoaderAware {
- //......
- @Bean
- public RedisTemplate<String,ExpiringSession> sessionRedisTemplate(RedisConnectionFactory connectionFactory) {
- //......
- return template;
- }
- @Bean
- public RedisOperationsSessionRepository sessionRepository(RedisTemplate<String, ExpiringSession> sessionRedisTemplate) {
- //......
- return sessionRepository;
- }
- @Bean
- public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository, ServletContext servletContext) {
- //......
- return sessionRepositoryFilter;
- }
- //......
- }
(2)集成spring-sesion的第四步中,我们编写了一个SpringSessionInitializer 类,它继承自AbstractHttpSessionApplicationInitializer。该类不需要重载或实现任何方法,它的作用是在Servlet容器初始化时,从Spring容器中获取一个默认名叫sessionRepositoryFilter的过滤器类(之前没有注册的话这里找不到会报错),并添加到Servlet过滤器链中。
- package org.springframework.session.web.context;
- /**
- * Registers the {@link DelegatingFilterProxy} to use the
- * springSessionRepositoryFilter before any other registered {@link Filter}.
- *
- * ......
- */
- @Order(100)
- public abstract class AbstractHttpSessionApplicationInitializer implements WebApplicationInitializer {
- private static final String SERVLET_CONTEXT_PREFIX = "org.springframework.web.servlet.FrameworkServlet.CONTEXT.";
- public static final String DEFAULT_FILTER_NAME = "springSessionRepositoryFilter";
- //......
- public void onStartup(ServletContext servletContext)
- throws ServletException {
- beforeSessionRepositoryFilter(servletContext);
- if(configurationClasses != null) {
- AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
- rootAppContext.register(configurationClasses);
- servletContext.addListener(new ContextLoaderListener(rootAppContext));
- }
- insertSessionRepositoryFilter(servletContext);//注册一个SessionRepositoryFilter
- afterSessionRepositoryFilter(servletContext);
- }
- /**
- * Registers the springSessionRepositoryFilter
- * @param servletContext the {@link ServletContext}
- */
- private void insertSessionRepositoryFilter(ServletContext servletContext) {
- String filterName = DEFAULT_FILTER_NAME;//默认名字是springSessionRepositoryFilter
- DelegatingFilterProxy springSessionRepositoryFilter = new DelegatingFilterProxy(filterName);//该Filter代理会在初始化时从Spring容器中查找springSessionRepositoryFilter,之后实际会使用SessionRepositoryFilter进行doFilter操作
- String contextAttribute = getWebApplicationContextAttribute();
- if(contextAttribute != null) {
- springSessionRepositoryFilter.setContextAttribute(contextAttribute);
- }
- registerFilter(servletContext, true, filterName, springSessionRepositoryFilter);
- }
- //......
- }
SessionRepositoryFilter是一个优先级最高的javax.servlet.Filter,它使用了一个SessionRepositoryRequestWrapper类接管了Http Session的创建和管理工作。
注意下面给出的是简化过的示例代码,与spring-session项目的源代码有所差异。
- @Order(SessionRepositoryFilter.DEFAULT_ORDER)
- public class SessionRepositoryFilter implements Filter {
- public doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
- HttpServletRequest httpRequest = (HttpServletRequest) request;
- SessionRepositoryRequestWrapper customRequest =
- new SessionRepositoryRequestWrapper(httpRequest);
- chain.doFilter(customRequest, response, chain);
- }
- // ...
- }
- public class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
- public SessionRepositoryRequestWrapper(HttpServletRequest original) {
- super(original);
- }
- public HttpSession getSession() {
- return getSession(true);
- }
- public HttpSession getSession(boolean createNew) {
- // create an HttpSession implementation from Spring Session
- }
- // ... other methods delegate to the original HttpServletRequest ...
- }
- WebInitializer,负责加载配置类。它继承自AbstractAnnotationConfigDispatcherServletInitializer,实现了WebApplicationInitializer接口
- SpringSessionInitializer,负责添加sessionRepositoryFilter的过滤器类。它继承自AbstractHttpSessionApplicationInitializer,实现了WebApplicationInitializer接口
在Servlet3.0规范中,Servlet容器启动时会自动扫描javax.servlet.ServletContainerInitializer的实现类,在实现类中我们可以定制需要加载的类。在spring-web项目中,有一个ServletContainerInitializer实现类SpringServletContainerInitializer,它通过注解@HandlesTypes(WebApplicationInitializer.class),让Servlet容器在启动该类时,会自动寻找所有的WebApplicationInitializer实现类。
- package org.springframework.web;
- @HandlesTypes(WebApplicationInitializer.class)
- public class SpringServletContainerInitializer implements ServletContainerInitializer {
- /**
- * Delegate the {@code ServletContext} to any {@link WebApplicationInitializer}
- * implementations present on the application classpath.
- *
- * <p>Because this class declares @{@code HandlesTypes(WebApplicationInitializer.class)},
- * Servlet 3.0+ containers will automatically scan the classpath for implementations
- * of Spring's {@code WebApplicationInitializer} interface and provide the set of all
- * such types to the {@code webAppInitializerClasses} parameter of this method.
- *
- * <p>If no {@code WebApplicationInitializer} implementations are found on the
- * classpath, this method is effectively a no-op. An INFO-level log message will be
- * issued notifying the user that the {@code ServletContainerInitializer} has indeed
- * been invoked but that no {@code WebApplicationInitializer} implementations were
- * found.
- *
- * <p>Assuming that one or more {@code WebApplicationInitializer} types are detected,
- * they will be instantiated (and <em>sorted</em> if the @{@link
- * org.springframework.core.annotation.Order @Order} annotation is present or
- * the {@link org.springframework.core.Ordered Ordered} interface has been
- * implemented). Then the {@link WebApplicationInitializer#onStartup(ServletContext)}
- * method will be invoked on each instance, delegating the {@code ServletContext} such
- * that each instance may register and configure servlets such as Spring's
- * {@code DispatcherServlet}, listeners such as Spring's {@code ContextLoaderListener},
- * or any other Servlet API componentry such as filters.
- *
- * @param webAppInitializerClasses all implementations of
- * {@link WebApplicationInitializer} found on the application classpath
- * @param servletContext the servlet context to be initialized
- * @see WebApplicationInitializer#onStartup(ServletContext)
- * @see AnnotationAwareOrderComparator
- */
- @Override
- public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
- throws ServletException {
- //......
- }
- }
5. 如何在Redis中查看Session数据?
(1)Http Session数据在Redis中是以Hash结构存储的。
- 127.0.0.1:6379> keys *
- 1) "spring:session:expirations:1431577740000"
- 2) "spring:session:sessions:e2cef3ae-c8ea-4346-ba6b-9b3b26eee578"
- 127.0.0.1:6379> type spring:session:sessions:e2cef3ae-c8ea-4346-ba6b-9b3b26eee578
- hash
- 127.0.0.1:6379> type spring:session:expirations:1431577740000
- set
- 127.0.0.1:6379> keys *
- 1) "spring:session:expirations:1431527520000"
- 2) "spring:session:sessions:59f3987c-d1e4-44b3-a83a-32079942888b"
- 3) "spring:session:sessions:11a69da6-138b-42bc-9916-60ae78aa55aa"
- 4) "spring:session:sessions:0a51e2c2-4a3b-4986-a754-d886d8a5d42d"
- 5) "spring:session:expirations:1431527460000"
- 127.0.0.1:6379> hkeys spring:session:sessions:59f3987c-d1e4-44b3-a83a-32079942888b
- 1) "maxInactiveInterval"
- 2) "creationTime"
- 3) "lastAccessedTime"
- 4) "sessionAttr:attr1"
- 127.0.0.1:6379> hget spring:session:sessions:59f3987c-d1e4-44b3-a83a-32079942888b sessionAttr:attr1
- "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x03"
- 127.0.0.1:6379> hget spring:session:sessions:59f3987c-d1e4-44b3-a83a-32079942888b creationTime
- "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01MM\x94(\xec"
6.参考文章
微信公众号: 架构师日常笔记 欢迎关注!