基於redis實現tomcat8的tomcat叢集的session持久化實現(tomcat-redis-session-manager二次開發)
阿新 • • 發佈:2018-12-31
前言:
本專案是基於jcoleman的tomcat-redis-session-manager二次開發版本
1、修改了小部分實現邏輯
2、去除對juni.jar包的依賴
3、去除無效程式碼和老版本tomcat操作API
4、支援tomcat 8 ,更高版本未測試
原始碼提供:
下載目錄:
注意:本專案依賴5個jar包,tomcat-api.jar;catalina.jar;servlet-api.jar;jedis-2.9.0.jar;commons-pool-2.4.2.jar,其中tomcat-api.jar、catalina.jar和servlet-api.jar這三個包是tomcat原生jar包,本專案打包時不需要打入這三個包
一、主要程式碼實現
1、session管理器實現
該類用於實現session的基本增刪改查操作,加入了redis實現持久化
package cn.eguid.redisSessionManager; import org.apache.catalina.Lifecycle; import org.apache.catalina.LifecycleException; import org.apache.catalina.LifecycleListener; import org.apache.catalina.util.LifecycleSupport; import org.apache.catalina.LifecycleState; import org.apache.catalina.Valve; import org.apache.catalina.Session; import org.apache.catalina.session.ManagerBase; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.Jedis; import redis.clients.jedis.Protocol; import java.io.IOException; import java.util.Arrays; import java.util.Set; /** * * @author eguid * */ public class RedisSessionManager extends ManagerBase implements Lifecycle { protected byte[] NULL_SESSION = "null".getBytes(); protected String host = "localhost"; protected int port = 6379; protected int database = 0; protected String password = null; protected int timeout = Protocol.DEFAULT_TIMEOUT; protected JedisPool connectionPool = null; protected RedisSessionHandlerValve handlerValve; protected ThreadLocal<RedisSession> currentSession = new ThreadLocal<RedisSession>(); protected ThreadLocal<String> currentSessionId = new ThreadLocal<String>(); protected ThreadLocal<Boolean> currentSessionIsPersisted = new ThreadLocal<Boolean>(); protected Serializer serializer; protected static String name = "RedisSessionManager"; // 用於序列化的類 protected String serializationStrategyClass = "cn.eguid.redisSessionManager.JavaSerializer"; protected LifecycleSupport lifecycle = new LifecycleSupport(this); public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } public int getDatabase() { return database; } public void setDatabase(int database) { this.database = database; } public int getTimeout() { return timeout; } public void setTimeout(int timeout) { this.timeout = timeout; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public void setSerializationStrategyClass(String strategy) { this.serializationStrategyClass = strategy; } @Override public int getRejectedSessions() { // Essentially do nothing. return 0; } public void setRejectedSessions(int i) { // Do nothing. } protected Jedis getConnection() { System.out.println("獲取jedis連線"); Jedis jedis = connectionPool.getResource(); if (getDatabase() != 0) { jedis.select(getDatabase()); } return jedis; } protected void returnConnection(Jedis jedis) { System.out.println("登出jedis連線"); jedis.close(); } @Override public void load() throws ClassNotFoundException, IOException { } @Override public void unload() throws IOException { } /** * Add a lifecycle event listener to this component. * * @param listener * The listener to add */ @Override public void addLifecycleListener(LifecycleListener listener) { lifecycle.addLifecycleListener(listener); } /** * Get the lifecycle listeners associated with this lifecycle. If this * Lifecycle has no listeners registered, a zero-length array is returned. */ @Override public LifecycleListener[] findLifecycleListeners() { return lifecycle.findLifecycleListeners(); } /** * Remove a lifecycle event listener from this component. * * @param listener * The listener to remove */ @Override public void removeLifecycleListener(LifecycleListener listener) { lifecycle.removeLifecycleListener(listener); } /** * Start this component and implement the requirements of * {@link org.apache.catalina.util.LifecycleBase#startInternal()}. * * @exception LifecycleException * if this component detects a fatal error that prevents this * component from being used */ @Override protected synchronized void startInternal() throws LifecycleException { boolean isSucc=false; try { System.out.println("準備開啟redis-session-Manager處理器 ... "); super.startInternal(); setState(LifecycleState.STARTING); Boolean attachedToValve = false; Valve[] values = getContainer().getPipeline().getValves(); for (Valve valve : values) { if (valve instanceof RedisSessionHandlerValve) { System.out.println("初始化redis-session-Manager處理器 ... "); this.handlerValve = (RedisSessionHandlerValve) valve; this.handlerValve.setRedisSessionManager(this); attachedToValve = true; break; } } if (!attachedToValve) { String error = "重大錯誤:redis-session-Manager無法新增到會話處理器,session在請求後不能正常啟動處理器!"; throw new LifecycleException(error); } System.out.println("初始化序列化器和反序列化器 ... "); initializeSerializer(); initializeDatabaseConnection(); setDistributable(true); isSucc=true; } catch (ClassNotFoundException e) { throw new LifecycleException(e); } catch (InstantiationException e) { throw new LifecycleException(e); } catch (IllegalAccessException e) { throw new LifecycleException(e); } catch(Exception e){ throw e; }finally{ if(isSucc){ System.out.println("redis-session-manager初始化成功"); }else{ System.out.println("redis-session-manager初始化失敗"); } } } /** * Stop this component and implement the requirements of * {@link org.apache.catalina.util.LifecycleBase#stopInternal()}. * * @exception LifecycleException * if this component detects a fatal error that prevents this * component from being used */ @Override protected synchronized void stopInternal() throws LifecycleException { System.err.println("停止redis-session-manager處理器!"); setState(LifecycleState.STOPPING); try { if (connectionPool != null) { connectionPool.destroy(); } } catch (Exception e) { System.err.println("登出redis連線池失敗!"); connectionPool = null; } super.stopInternal(); } @Override public Session createSession(String sessionId) { System.out.println("根據sessionId建立session:" + sessionId); // 初始化設定並建立一個新的session返回 RedisSession session = (RedisSession) createEmptySession(); session.setNew(true); session.setValid(true); session.setCreationTime(System.currentTimeMillis()); session.setMaxInactiveInterval(getMaxInactiveInterval()); String jvmRoute = getJvmRoute(); Jedis jedis = null; try { jedis = getConnection(); do { if (null == sessionId) { // 重新生成一個sessionId sessionId = generateSessionId(); } if (jvmRoute != null) { sessionId += '.' + jvmRoute; } } while (jedis.setnx(sessionId.getBytes(), NULL_SESSION) == 1L); /* * Even though the key is set in Redis, we are not going to flag the * current thread as having had the session persisted since the * session isn't actually serialized to Redis yet. This ensures that * the save(session) at the end of the request will serialize the * session into Redis with 'set' instead of 'setnx'. */ session.setId(sessionId); session.tellNew(); currentSession.set(session); currentSessionId.set(sessionId); currentSessionIsPersisted.set(false); } finally { if (jedis != null) { jedis.close(); } } return session; } @Override public Session createEmptySession() { System.out.println("新增空的session"); return new RedisSession(this); } @Override public void add(Session session) { System.out.println("新增session到redis資料庫"); try { save(session); } catch (IOException e) { throw new RuntimeException("儲存session失敗", e); } } @Override public Session findSession(String id) throws IOException { System.out.println("查詢sessionId:" + id); RedisSession session = null; if (id == null) { session = null; currentSessionIsPersisted.set(false); } else if (id.equals(currentSessionId.get())) { session = currentSession.get(); } else { session = loadSessionFromRedis(id); if (session != null) { currentSessionIsPersisted.set(true); } } currentSession.set(session); currentSessionId.set(id); return session; } public void clear() { Jedis jedis = null; try { jedis = getConnection(); jedis.flushDB(); } finally { if (jedis != null) { jedis.close(); } } } public int getSize() throws IOException { Jedis jedis = null; try { jedis = getConnection(); int size = jedis.dbSize().intValue(); return size; } finally { if (jedis != null) { jedis.close(); } } } public String[] keys() throws IOException { Jedis jedis = null; try { jedis = getConnection(); Set<String> keySet = jedis.keys("*"); return keySet.toArray(new String[keySet.size()]); } finally { if (jedis != null) { jedis.close(); } } } public RedisSession loadSessionFromRedis(String id) throws IOException { RedisSession session; Jedis jedis = null; try { jedis = getConnection(); byte[] data = jedis.get(id.getBytes()); if (data == null) { session = null; } else if (Arrays.equals(NULL_SESSION, data)) { throw new IllegalStateException("Race condition encountered: attempted to load session[" + id + "] which has been created but not yet serialized."); } else { session = (RedisSession) createEmptySession(); serializer.deserializeInto(data, session); session.setId(id); session.setNew(false); session.setMaxInactiveInterval(getMaxInactiveInterval() * 1000); session.access(); session.setValid(true); session.resetDirtyTracking(); } return session; } catch (IOException e) { throw e; } catch (ClassNotFoundException ex) { throw new IOException("Unable to deserialize into session", ex); } finally { if (jedis != null) { jedis.close(); } } } /** * save session to redis * * @param session * @throws IOException */ public void save(Session session) throws IOException { System.out.println("儲存session到redis"); Jedis jedis = null; try { RedisSession redisSession = (RedisSession) session; Boolean sessionIsDirty = redisSession.isDirty(); redisSession.resetDirtyTracking(); byte[] binaryId = redisSession.getId().getBytes(); jedis = getConnection(); if (sessionIsDirty || currentSessionIsPersisted.get() != true) { jedis.set(binaryId, serializer.serializeFrom(redisSession)); } currentSessionIsPersisted.set(true); jedis.expire(binaryId, getMaxInactiveInterval()); } catch (IOException e) { throw e; } finally { if (jedis != null) { jedis.close(); } } } @Override public void remove(Session session) { remove(session, false); } @Override public void remove(Session session, boolean update) { System.out.println("刪除redis中的session,更新:"+update); Jedis jedis = null; try { jedis = getConnection(); jedis.del(session.getId()); } finally { if (jedis != null) { jedis.close(); } } } public void afterRequest() { System.out.println("刪除快取在記憶體中的session"); RedisSession redisSession = currentSession.get(); if (redisSession != null) { currentSession.remove(); currentSessionId.remove(); currentSessionIsPersisted.remove(); } } @Override public void processExpires() { // We are going to use Redis's ability to expire keys for session // expiration. // Do nothing. } private void initializeDatabaseConnection() throws LifecycleException { try { System.out.println("初始化redis連線池 ... "); // 初始化redis連線池 connectionPool = new JedisPool(new JedisPoolConfig(), getHost(), getPort(), getTimeout(), getPassword()); } catch (Exception e) { e.printStackTrace(); throw new LifecycleException("redis連線池初始化錯誤,redis不存在或配置錯誤!", e); } } private void initializeSerializer() throws InstantiationException, IllegalAccessException, ClassNotFoundException { System.out.println("準備初始化序列器 ... "); serializer = (Serializer) Class.forName(serializationStrategyClass).newInstance(); ClassLoader classLoader = null; if (getContainer() != null) { classLoader = getContainer().getClass().getClassLoader(); } System.out.println("初始化序列器完成!"); serializer.setClassLoader(classLoader); } }
2、redis的session實現
package cn.eguid.redisSessionManager; import java.security.Principal; import org.apache.catalina.Manager; import org.apache.catalina.session.StandardSession; import java.util.HashMap; public class RedisSession extends StandardSession { protected static Boolean manualDirtyTrackingSupportEnabled = false; public static void setManualDirtyTrackingSupportEnabled(Boolean enabled) { manualDirtyTrackingSupportEnabled = enabled; } protected static String manualDirtyTrackingAttributeKey = "__changed__"; public static void setManualDirtyTrackingAttributeKey(String key) { manualDirtyTrackingAttributeKey = key; } protected HashMap<String, Object> changedAttributes; protected Boolean dirty; public RedisSession(Manager manager) { super(manager); resetDirtyTracking(); } public Boolean isDirty() { return dirty || !changedAttributes.isEmpty(); } public HashMap<String, Object> getChangedAttributes() { return changedAttributes; } public void resetDirtyTracking() { changedAttributes = new HashMap<String, Object>(); dirty = false; } @Override public void setAttribute(String key, Object value) { if (manualDirtyTrackingSupportEnabled && manualDirtyTrackingAttributeKey.equals(key)) { dirty = true; return; } Object oldValue = getAttribute(key); if ( value == null && oldValue != null || oldValue == null && value != null || !value.getClass().isInstance(oldValue) || !value.equals(oldValue) ) { changedAttributes.put(key, value); } super.setAttribute(key, value); } @Override public void removeAttribute(String name) { dirty = true; super.removeAttribute(name); } @Override public void setId(String id) { this.id = id; } @Override public void setPrincipal(Principal principal) { dirty = true; super.setPrincipal(principal); } }
3、session處理器實現
該類可以用於在請求前後請求後做一些操作,不僅侷限於session操作,可以做servlet中的所有操作
package cn.eguid.redisSessionManager;
import org.apache.catalina.Session;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import javax.servlet.ServletException;
import java.io.IOException;
public class RedisSessionHandlerValve extends ValveBase {
// redis-session-manager管理器操作
private RedisSessionManager manager;
// 通過tomcat的context.xml可以注入該例項
public void setRedisSessionManager(RedisSessionManager manager) {
this.manager = manager;
}
// 產生一個請求後
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
try {
getNext().invoke(request, response);
} finally {
System.out.println("請求完畢後,redis-session-manager正在獲取當前產生的session");
Session session = request.getSessionInternal(false);
storeOrRemoveSession(session);
System.out.println("redis-session-manager操作結束,正在清理記憶體中的session!");
// 刪除記憶體中的session
manager.afterRequest();
}
}
private void storeOrRemoveSession(Session session) {
try {
if (session!=null && session.isValid() && session.getSession() != null) {
manager.save(session);
} else {
manager.remove(session);
}
} catch (Exception e) {
System.err.println("提示一下:session操作失敗");
}
}
}
二、如何配置該專案到tomcat
1、拷貝tomcat-redis-session-manager-by-eguid.jar,jedis-2.9.0.jar,commons-pool2-2.2.jar到tomcat/lib目錄下
2、修改Tomcat context.xml (or the context block of the server.xml if applicable.)
<Valve className="cn.eguid.redisSessionManager.RedisSessionHandlerValve"/>
<Manager className="cn.eguid.redisSessionManager.RedisSessionManager"
host="192.168.30.21"
port="6379"
database="14"
maxInactiveInterval="1800"/>