利用spring session解決共享Session問題
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容器的程式碼。這方面其實早就有開源專案了,例如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 ...
}
(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"