当前位置:首页 > Spring > 正文内容

利用springsession解决共享Session问题

canca9年前 (2016-07-21)Spring416
1.共享Session问题

HttpSession是通过Servlet容器创建和管理的,像Tomcat/Jetty都是保存在内存中的。而如果我们把web服务器搭建成 分布式的集群,然后利用LVS或Nginx做负载均衡,那么来自同一用户的Http请求将有可能被分发到两个不同的web站点中去。那么问题就来了,如何 保证不同的web站点能够共享同一份session数据呢?


最简单的想法就是把session数据保存到内存以外的一个统一的地方,例如Memcached/Redis等数据库中。那么问题又来了,如何替换掉Servlet容器创建和管理HttpSession的实现呢?

(1)设计一个Filter,利用HttpServletRequestWrapper,实现自己的 getSession()方法,接管创建和管理Session数据的工作。spring-session就是通过这样的思路实现的。
(2)利用Servlet容器提供的插件功能,自定义HttpSession的创建和管理策略,并通过配置的方式替换掉默认的策略。不过这种方式有个缺点,就是需要耦合Tomcat/Jetty等Servlet容器的代码。这方面其实早就有开源项目了,例如 memcached-session-manager,以及 tomcat-redis-session-manager。暂时都只支持Tomcat6/Tomcat7。

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项目:

    利用springsession解决共享Session问题 - Mr.Can - Mr.Cans Blog
     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 ... }

    (3)好了,剩下的问题就是,如何在Servlet容器启动时,加载下面两个类。幸运的是,这两个类由于都实现了 WebApplicationInitializer接口,会被自动加载。
  • 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结构存储的。


    (2)可以看到,还有一个key="spring:session:expirations:1431577740000"的数据,是以Set结构保存的。这个值记录了所有session数据应该被删除的时间(即最新的一个session数据过期的时间)。

    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.参考文章

    Spring Session 1.01 Reference

    spring session入门

    集群session共享机制

    扫描二维码推送至手机访问。

    版权声明:本文由Ant.Master's Blog发布,如需转载请注明出处。

    本文链接:https://iant.work/post/44.html

    标签: Spring
    分享给朋友:

    “利用springsession解决共享Session问题” 的相关文章

    Spring IOC配置文件5分钟搞掂

    以下并非普通的Spring配置文件。下面这个文件就是Spring IOC的全部应用例子。 <?xml version="1.0" encoding="utf-8"?><!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "htt...

    servlet中取到Spring配置

    转自:http://blog.csdn.net/sd_lichangyou/archive/2006/10/25/1350519.aspx --------------------------------------------------------------------------------...

    IOC容器,其实很简单

      前几天看easyjweb代码,看到容器那块,顺便自己也写了个简单的IOC容器练练手。今天正好有时间就写出来大家看看,我也好冒充一下高手。 这是个很简单的IOC容器,基本功能有:自动加载所有使用了@Inject注解的类,然后注入每个类里边使用了@Inject注解的字段。 原理很简单,就是...

    Spring RMI 支持

    RMI是从JDK 1.1开始就出现的API功能,它让客户端在使用远程对象所提供的服务时,就如何使用本地对象一样,然而RMI在使用时必须一连串繁复的手续,像是服务介 面在定义时必须继承java.rmi.Remote接口、服务Server在实作时必须继承java.rmi.UnicastRemoteObj...

    springBoot项目自定义命名application.properties配置文件名称

    为⽅便期间,更改名称后的properties⽂件仍然放置在resource下(相当于classpa:/ 在classpath的根⽬录下)即可;解决⽅法解决⽅法⼀:将更改properties⽂件名的项⽬使⽤Maven⼯具打成JAR包,然后在DOS命令⾏启动项⽬:java -jar ...

    发表评论

    访客

    ◎欢迎参与讨论,请在这里发表您的看法和观点。