Skip to content

Instantly share code, notes, and snippets.

@caoxudong
Created March 31, 2015 04:03
Show Gist options
  • Save caoxudong/87c54be9105bc8cf18b7 to your computer and use it in GitHub Desktop.
Save caoxudong/87c54be9105bc8cf18b7 to your computer and use it in GitHub Desktop.
Memcached-based shared session
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.1.xsd">
<bean id="memcachedPlainCallbackHandler"
class="net.spy.memcached.auth.PlainCallbackHandler">
<constructor-arg><value>${ocs.username}</value></constructor-arg>
<constructor-arg><value>${ocs.password}</value></constructor-arg>
</bean>
<bean id="authDescriptor" class="net.spy.memcached.auth.AuthDescriptor">
<constructor-arg>
<array><value>PLAIN</value></array>
</constructor-arg>
<constructor-arg><ref bean="memcachedPlainCallbackHandler"/></constructor-arg>
</bean>
<bean id="memcachedClient"
class="net.spy.memcached.spring.MemcachedClientFactoryBean">
<property name="servers" value="${ocs.host}:${ocs.port}"/>
<property name="protocol" value="BINARY"/>
<property name="transcoder">
<bean class="net.spy.memcached.transcoders.SerializingTranscoder">
<property name="compressionThreshold" value="1024"/>
</bean>
</property>
<property name="opTimeout" value="3000"/>
<property name="timeoutExceptionThreshold" value="1998"/>
<property name="hashAlg">
<value type="net.spy.memcached.DefaultHashAlgorithm">KETAMA_HASH</value>
</property>
<property name="locatorType" value="CONSISTENT"/>
<property name="failureMode" value="Redistribute"/>
<property name="useNagleAlgorithm" value="false"/>
<property name="authDescriptor" ref="authDescriptor"/>
<property name="needAuth" value="${ocs.needAuth}"/>
</bean>
<bean id="configMemcachedPlainCallbackHandler"
class="net.spy.memcached.auth.PlainCallbackHandler">
<constructor-arg><value>${config.data.ocs.username}</value></constructor-arg>
<constructor-arg><value>${config.data.ocs.password}</value></constructor-arg>
</bean>
<bean id="configAuthDescriptor" class="net.spy.memcached.auth.AuthDescriptor">
<constructor-arg>
<array><value>PLAIN</value></array>
</constructor-arg>
<constructor-arg><ref bean="configMemcachedPlainCallbackHandler"/></constructor-arg>
</bean>
<bean id="configMemcachedClient"
class="net.spy.memcached.spring.MemcachedClientFactoryBean">
<property name="servers" value="${config.data.ocs.host}:${config.data.ocs.port}"/>
<property name="protocol" value="BINARY"/>
<property name="transcoder">
<bean class="net.spy.memcached.transcoders.SerializingTranscoder">
<property name="compressionThreshold" value="1024"/>
</bean>
</property>
<property name="opTimeout" value="3000"/>
<property name="timeoutExceptionThreshold" value="1998"/>
<property name="hashAlg">
<value type="net.spy.memcached.DefaultHashAlgorithm">KETAMA_HASH</value>
</property>
<property name="locatorType" value="CONSISTENT"/>
<property name="failureMode" value="Redistribute"/>
<property name="useNagleAlgorithm" value="false"/>
<property name="authDescriptor" ref="configAuthDescriptor"/>
<property name="needAuth" value="${config.data.ocs.needAuth}"/>
</bean>
</beans>
package appcloud.common.util.web.session.sharedsession;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import com.alibaba.fastjson.JSON;
import net.spy.memcached.MemcachedClient;
/**
* <p>获取共享session的过滤器。应该在使用session对象之前,先经过该过滤器。
*
* <p>共享session存储在memcached中。
*
* <p>该过滤器会使用以下应用初始化参数:
* <ul>
* <li>sessionTimeout: 用于控制session的有效期,单位为秒,默认值为30分钟。</li>
* <li>memcachedClientBeanId: 配置于spring中的MemCachedClient对象的bean的id,
* 默认为"memcachedClient"</li>
* <li>sessionKeyPrefix: 用于设置memcached中session对象key的前缀,默认值为"session_"</li>
* </ul>
*
* <p>针对该过滤器,可以设置以下初始化参数:
* <ul>
* <li>sessionIdName: 指定用于记录sessionId的cookie的名字,默认值为"sessid"</li>
* </ul>
*
* @author caoxudong
* @since 0.1.0
*/
public class MemcachedSessionFilter implements Filter {
private static Logger logger = LogManager.getLogger();
// session有效期,单位是秒
private static final String CONFIG_PROP_NAME_SESSION_TIMEOUT =
"sessionTimeout";
private int sessionTimeout = 30 * 60;
// session id属性的名字,使用该名字在cookie中查找对应的属性值
private static final String CONFIG_PROP_NAME_SESSION_ID = "sessionIdName";
private String sessionIdName = "sessid";
// spring中memcachhedClient对象的id
private static final String CONFIG_PROP_NAME_MEMCACHED_CLIENT_BEAN_ID =
"memcachedClientBeanId";
private String memcachedClientBeanId = "memcachedClient";
// memcached中session缓存的key的前缀
private static final String CONFIG_PROP_NAME_SESSION_KEY_PREFIX =
"sessionKeyPrefix";
private String sessionKeyPrefix = "session_";
// 通过memcached存储共享session
private MemcachedClient memcachedClient;
// 应用上下文
private ServletContext servletContext;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 解析session有效期
this.servletContext = filterConfig.getServletContext();
String interval =
this.servletContext.getInitParameter(CONFIG_PROP_NAME_SESSION_TIMEOUT);
if ((interval != null) && (!"".equals(interval))) {
sessionTimeout = Integer.parseInt(interval);
}
SessionWrapper.defaultSessionTimeout = sessionTimeout;
// 解析session缓存的key前缀
String sessionKeyPre =
this.servletContext.getInitParameter(
CONFIG_PROP_NAME_SESSION_KEY_PREFIX);
if ((null != sessionKeyPre) && ("".equals(sessionKeyPre))) {
this.sessionKeyPrefix = sessionKeyPre;
}
// 获取memcachedClient对象的id
String initMemcachedClientBeanId =
this.servletContext.getInitParameter(
CONFIG_PROP_NAME_MEMCACHED_CLIENT_BEAN_ID);
if ((initMemcachedClientBeanId != null)
&& (!"".equals(initMemcachedClientBeanId))) {
this.memcachedClientBeanId = initMemcachedClientBeanId;
}
// 设置memcachedClient
WebApplicationContext wac =
WebApplicationContextUtils.getRequiredWebApplicationContext(
servletContext);
memcachedClient = (MemcachedClient)wac.getBean(this.memcachedClientBeanId);
MemcachedSessionListener.memcachedClient = memcachedClient;
// 解析sessionId的名字
String tempSessionIdName =
filterConfig.getInitParameter(CONFIG_PROP_NAME_SESSION_ID);
if ((tempSessionIdName != null) && (!"".equals(tempSessionIdName))) {
sessionIdName = tempSessionIdName;
}
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)resp;
RequestWrapper requestWrapper = null;
// 查找是否已经设置过sessionID
Cookie[] cookies = request.getCookies();
logger.debug("Received cookies: " + JSON.toJSONString(cookies));
Cookie sessionIdCookie = null;
String sessionId = null;
if ((cookies != null) && (cookies.length > 0)) {
for (Cookie cookie: cookies) {
String name = cookie.getName();
String value = cookie.getValue();
if (sessionIdName.equals(name)) {
sessionIdCookie = cookie;
sessionId = value;
break;
}
}
}
/**
* 1. 若没设置过sessionId,则表示该用户之前没有访问过,不必理会session
* 2. 若设置过sessionId,则根据sessionId,从memcached中获取缓存的session数据
* 2.1 若不存在(没有或已失效)session数据,则不必理会session
* 2.2 若存在session数据,则以sessionId和session数据创建SessionWrapper对象
*
* 这里实际上是以memcached中是否存在数据来判断session是否失效,因而导致
*/
SessionWrapper session = null;
if (sessionIdCookie == null) {
// 之前没有访问过
requestWrapper = new RequestWrapper(request);
} else {
// 之前已经访问过的用户
// 先从memcache中获取session对象
session =
(SessionWrapper)memcachedClient.get(sessionKeyPrefix + sessionId);
logger.debug("Received memcahed session: " + JSON.toJSONString(session));
if (session == null) {
// 没有或者已经过期
requestWrapper = new RequestWrapper(request);
} else {
session.setNew(false);
session.setLastAccessedTime(System.currentTimeMillis());
requestWrapper = new RequestWrapper(request, session);
}
}
// 执行其他业务
chain.doFilter(requestWrapper, response);
// 如果应用使用到了session,将session保存到memcache中,并将sessionId写入到cookie
HttpSession sessionAfterService = requestWrapper.getSession(false);
if (sessionAfterService != null) {
String sidAfterService = sessionAfterService.getId();
Cookie sessionIdCookieAfterService =
new Cookie(this.sessionIdName, sidAfterService);
sessionIdCookieAfterService.setHttpOnly(true);
sessionIdCookieAfterService.setPath("/");
sessionIdCookieAfterService.setMaxAge(this.sessionTimeout);
response.addCookie(sessionIdCookieAfterService);
memcachedClient.set(
sessionKeyPrefix + sidAfterService,
sessionTimeout,
sessionAfterService);
logger.debug("Stored memcached session: " + JSON.toJSONString(session));
}
}
@Override
public void destroy() {
}
}
package appcloud.common.util.web.session.sharedsession;
import net.spy.memcached.MemcachedClient;
import appcloud.common.util.web.session.SessionEvent;
import appcloud.common.util.web.session.SessionEventType;
import appcloud.common.util.web.session.SessionListener;
/**
* 根据不同的session事件,对memcached中缓存的session对象做处理
*
* @author caoxudong
* @since 0.1.0
*/
public class MemcachedSessionListener implements SessionListener {
/**
* 该字段的值由{@link MemcachedSessionFilter}在初始化时设置
*
* FIXME: 这个设置太别扭
*/
public static MemcachedClient memcachedClient;
@Override
public void fire(SessionEvent event) {
SessionEventType eventType = event.getType();
switch (eventType) {
case INVALIDATE: {
/**
* 需要将memcached中的session置为失效
*/
SessionWrapper session = (SessionWrapper)event.getData();
String sid = session.getId();
memcachedClient.delete(sid);
break;
}
case CREATE:
break;
default:
break;
}
}
}
package appcloud.common.util.web.session.sharedsession;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpSession;
public class RequestWrapper extends HttpServletRequestWrapper {
private SessionWrapper internalSession;
public RequestWrapper(HttpServletRequest request, SessionWrapper session) {
super(request);
if ((request == null) || (session == null)) {
throw new IllegalArgumentException(
"Arguments request or session cannot be null.");
}
this.internalSession = session;
}
public RequestWrapper(HttpServletRequest request) {
super(request);
if (request == null) {
throw new IllegalArgumentException("Arguments request cannot be null.");
}
}
@Override
public HttpSession getSession(boolean create) {
SessionWrapper session = null;
if (create) {
session = new SessionWrapper();
session.addSessionLister(new MemcachedSessionListener());
this.internalSession = session;
} else {
if (internalSession != null) {
session = internalSession;
}
}
return session;
}
@Override
public HttpSession getSession() {
if (this.internalSession == null) {
return this.getSession(true);
} else {
return this.internalSession;
}
}
@Override
public String changeSessionId() {
String newSessionId = SessionWrapper.generateSessionId();
this.internalSession.id = newSessionId;
return newSessionId;
}
}
package appcloud.common.util.web.session;
import java.util.EventObject;
import javax.servlet.http.HttpSession;
import com.alibaba.fastjson.JSON;
/**
* session中的事件对象
*
* @author caoxudong
* @since 0.1.0
*/
public final class SessionEvent extends EventObject {
private static final long serialVersionUID = 1L;
/**
* 事件数据
*/
private final Object data;
/**
* 发生事件的session对象
*/
private final HttpSession session;
/**
* 事件类型
*/
private final SessionEventType type;
public SessionEvent(HttpSession session, SessionEventType type, Object data) {
super(session);
this.session = session;
this.type = type;
this.data = data;
}
/**
* 获取事件数据
*/
public Object getData() {
return this.data;
}
/**
* 获取发生了事件的session对象
*/
public HttpSession getSession() {
return this.session;
}
/**
* 获取事件类型
*/
public SessionEventType getType() {
return this.type;
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
}
package appcloud.common.util.web.session;
/**
* session事件类型
*
* @author caoxudong
* @since 0.1.0
*/
public enum SessionEventType {
/**
* 创建session
*/
CREATE,
/**
* 销毁session
*/
//DESCTROY,
/**
* session属性变动
*/
//ATTRIBUTE_CHANGE,
/**
* 激活session
*/
//ACTIVATE,
/**
* 换出,即将session暂时从内存中排出,进行持久化
*/
//PASSIVATE,
/**
* session失效
*/
INVALIDATE;
}
package appcloud.common.util.web.session;
import appcloud.common.exception.AppCloudException;
/**
* session相关的异常
*
* @author caoxudong
* @since 0.1.0
*/
public class SessionException extends AppCloudException {
private static final long serialVersionUID = 1L;
public SessionException() {
super();
}
public SessionException(String message) {
super(message);
}
public SessionException(String message, Throwable cause) {
super(message, cause);
}
public SessionException(Throwable cause) {
super(cause);
}
}
package appcloud.common.util.web.session;
import java.util.EventListener;
public interface SessionListener extends EventListener {
public void fire(SessionEvent event);
}
package appcloud.common.util.web.session.sharedsession;
import java.io.Serializable;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionAttributeListener;
import javax.servlet.http.HttpSessionContext;
import org.apache.commons.codec.binary.Base64;
import appcloud.common.util.web.session.SessionEvent;
import appcloud.common.util.web.session.SessionEventType;
import appcloud.common.util.web.session.SessionListener;
/**
* <p>对{@link HttpSession}默认实现的包装,
* 将数据存储在内部的{@link SessionWrapper#data}中。
*
* <p>目前,有一些session相关的特性还没有实现,包括:
* <ul>
* <li>不支持SessionContext</li>
* <li>监听session属性的变动,不支持{@link HttpSessionAttributeListener}</li>
* <li>监听session属性的变动,不支持{@link HttpSessionAttributeListener}</li>
* </ul>
*
* TODO: 1.实现对session属性变动的监听; 2. 多线程安全性还未考虑太多
*
* @author caoxudong
* @since 0.1.0
* @see SessionEventType
*/
public class SessionWrapper implements HttpSession, Serializable {
private static final long serialVersionUID = 1L;
/**
* 默认情况下,session有效期为30分钟。可以在{@link MemcachedSessionFilter}中设置。
*
* FIXME: 这里的设置方式太别扭。
*/
public static volatile int defaultSessionTimeout = 30 * 60;
protected Map<String, Object> data = new ConcurrentHashMap<>();
protected transient List<SessionListener> listeners = new LinkedList<>();
protected boolean isNew = true;
protected boolean isValid = true;
protected String id;
protected long creationTime = System.currentTimeMillis();
protected long lastAccessedTime = this.creationTime;
protected int maxInactiveInterval = defaultSessionTimeout;
public SessionWrapper() {
super();
this.id = generateSessionId();
}
@Override
public long getCreationTime() {
checkValidity();
return this.creationTime;
}
@Override
public String getId() {
return this.id;
}
@Override
public long getLastAccessedTime() {
checkValidity();
return this.lastAccessedTime;
}
public void setLastAccessedTime(long lastAccessedTime) {
this.lastAccessedTime = lastAccessedTime;
}
@Override
public ServletContext getServletContext() {
return null;
}
@Override
public void setMaxInactiveInterval(int interval) {
this.maxInactiveInterval = interval;
}
@Override
public int getMaxInactiveInterval() {
return this.maxInactiveInterval;
}
@Override
public HttpSessionContext getSessionContext() {
return null;
}
@Override
public Object getAttribute(String name) {
checkValidity();
if (null == name) {
return null;
} else {
return this.data.get(name);
}
}
@Override
public Object getValue(String name) {
checkValidity();
if (null == name) {
return null;
} else {
return this.data.get(name);
}
}
@Override
public Enumeration<String> getAttributeNames() {
checkValidity();
Set<String> names = new HashSet<>();
names.addAll(this.data.keySet());
return Collections.enumeration(names);
}
@Override
public String[] getValueNames() {
checkValidity();
Set<String> keyNamesSet = this.data.keySet();
String[] keyNames = new String[keyNamesSet.size()];
keyNames = keyNamesSet.toArray(keyNames);
return keyNames;
}
@Override
public void setAttribute(String name, Object value) {
checkAttributeName(name);
checkValidity();
if (null == value) {
this.removeAttribute(name);
} else {
this.data.put(name, value);
}
}
@Override
public void putValue(String name, Object value) {
checkAttributeName(name);
checkValidity();
if (null == value) {
this.removeAttribute(name);
} else {
this.data.put(name, value);
}
}
@Override
public void removeAttribute(String name) {
checkValidity();
this.data.remove(name);
}
@Override
public void removeValue(String name) {
checkValidity();
this.data.remove(name);
}
@Override
public void invalidate() {
checkValidity();
this.data.clear();
this.isValid = true;
for (SessionListener listener: listeners) {
listener.fire(new SessionEvent(this, SessionEventType.INVALIDATE, this));
}
}
@Override
public boolean isNew() {
checkValidity();
return this.isNew;
}
public void setNew(boolean isNew) {
this.isNew = isNew;
}
/**
* 添加session事件监听器
*
* @param sessionListener 事件监听器
* @see SessionListener
* @see SessionEventType
*/
public void addSessionLister(SessionListener sessionListener) {
this.listeners.add(sessionListener);
}
/**
* 检查session是否已经失效了
*/
private void checkValidity() {
if (!isValid) {
throw new IllegalStateException("Session is invalid.");
}
}
/**
* 检查session属性的名字
* @param name session属性的名字
*/
private void checkAttributeName(String name) {
if (name == null) {
throw new IllegalArgumentException("Attribute name cannot be null.");
}
}
public static String generateSessionId() {
return Base64.encodeBase64String(UUID.randomUUID().toString().getBytes());
}
}
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<display-name>demo</display-name>
<!-- for shared session -->
<context-param>
<param-name>sessionTimeout</param-name>
<param-value>1800</param-value>
</context-param>
<context-param>
<param-name>memcachedClientBeanId</param-name>
<param-value>memcachedClient</param-value>
</context-param>
<!-- spring 配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- 强制字符转码 UTF8 -->
<filter>
<filter-name>springUtf8Encoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>springUtf8Encoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- ocs-based shared session -->
<filter>
<filter-name>ocsSessionFilter</filter-name>
<filter-class>appcloud.common.util.web.session.sharedsession.MemcachedSessionFilter</filter-class>
<init-param>
<param-name>sessionIdName</param-name>
<param-value>sessid</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>ocsSessionFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- spring上下文启动监听 -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!--订单定时器-->
<listener>
<listener-class>
appcloud.common.util.web.listener.OrderListener
</listener-class>
</listener>
<!-- spring-mvc servlet -->
<servlet>
<servlet-name>springMVCServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springMVCServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment