Java tutorial
/* * Copyright 2011 Alexander V. Zinin * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package ru.zinin.redis.session; import org.apache.catalina.*; import org.apache.catalina.mbeans.MBeanUtils; import org.apache.catalina.session.Constants; import org.apache.catalina.util.LifecycleMBeanBase; import org.apache.catalina.util.SessionIdGenerator; import org.apache.commons.pool.impl.GenericObjectPool; import org.apache.juli.logging.Log; import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.res.StringManager; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.Protocol; import javax.naming.InitialContext; import javax.naming.NamingException; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.IOException; import java.util.HashSet; import java.util.Set; import java.util.concurrent.Executors; /** * Date: 28.10.11 22:16 * * @author Alexander V. Zinin (mail@zinin.ru) */ public class RedisManager extends LifecycleMBeanBase implements Manager, PropertyChangeListener { private final Log log = LogFactory.getLog(RedisManager.class); private static final String info = "RedisManager/1.0"; private static final StringManager sm = StringManager.getManager(Constants.Package); private Container container; private int maxInactiveInterval = 30 * 60; private int sessionIdLength = 32; private int maxActiveSessions = -1; private String secureRandomClass = null; private String secureRandomAlgorithm = "SHA1PRNG"; private String secureRandomProvider = null; private SessionIdGenerator sessionIdGenerator = null; private String jedisJndiName = "pool/jedis"; private String redisHostname = "localhost"; private int redisPort = Protocol.DEFAULT_PORT; private int redisTimeout = Protocol.DEFAULT_TIMEOUT; private String redisPassword; private JedisPool pool; private boolean disableListeners = false; private int dbIndex = 0; private PropertyChangeSupport support = new PropertyChangeSupport(this); private final RedisEventListenerThread eventListenerThread = new RedisEventListenerThread(this); @Override public Container getContainer() { return container; } @Override public void setContainer(Container container) { log.trace(String.format("EXEC setContainer(%s);", container)); // De-register from the old Container (if any) if ((this.container != null) && (this.container instanceof Context)) { this.container.removePropertyChangeListener(this); } Container oldContainer = this.container; this.container = container; support.firePropertyChange("container", oldContainer, this.container); // Register with the new Container (if any) if ((this.container != null) && (this.container instanceof Context)) { setMaxInactiveInterval(((Context) this.container).getSessionTimeout() * 60); this.container.addPropertyChangeListener(this); } } @Override public boolean getDistributable() { log.trace("EXEC getDistributable();"); return true; } @Override public void setDistributable(boolean distributable) { log.trace(String.format("EXEC setDistributable(%s);", distributable)); if (!distributable) { log.error("Only distributable web applications supported."); } } @Override public String getInfo() { log.trace("EXEC getInfo();"); return info; } @Override public int getMaxInactiveInterval() { log.trace("EXEC getMaxInactiveInterval();"); return maxInactiveInterval; } @Override public void setMaxInactiveInterval(int interval) { log.trace(String.format("EXEC setMaxInactiveInterval(%d);", interval)); int oldMaxInactiveInterval = this.maxInactiveInterval; this.maxInactiveInterval = interval; support.firePropertyChange("maxInactiveInterval", Integer.valueOf(oldMaxInactiveInterval), Integer.valueOf(this.maxInactiveInterval)); } @Override public int getSessionIdLength() { log.trace("EXEC getSessionIdLength();"); return sessionIdLength; } @Override public void setSessionIdLength(int idLength) { log.trace(String.format("EXEC setSessionIdLength(%d);", idLength)); int oldSessionIdLength = this.sessionIdLength; this.sessionIdLength = idLength; support.firePropertyChange("sessionIdLength", Integer.valueOf(oldSessionIdLength), Integer.valueOf(this.sessionIdLength)); } @Override public long getSessionCounter() { log.trace("EXEC getSessionCounter();"); return 0; } @Override public void setSessionCounter(long sessionCounter) { log.trace(String.format("EXEC setSessionCounter(%d);", sessionCounter)); } @Override public int getMaxActive() { log.trace("EXEC getMaxActive();"); return 0; } @Override public void setMaxActive(int maxActive) { log.trace(String.format("EXEC setMaxActive(%d);", maxActive)); } public int getMaxActiveSessions() { log.trace("EXEC getMaxActiveSessions();"); return this.maxActiveSessions; } public void setMaxActiveSessions(int max) { log.trace(String.format("EXEC setMaxActiveSessions(%d);", max)); int oldMaxActiveSessions = this.maxActiveSessions; this.maxActiveSessions = max; support.firePropertyChange("maxActiveSessions", Integer.valueOf(oldMaxActiveSessions), Integer.valueOf(this.maxActiveSessions)); } @Override public int getActiveSessions() { log.trace("EXEC getActiveSessions();"); String key = RedisSessionKeys.getSessionsKey(); Long result; Jedis jedis = pool.getResource(); try { jedis.select(dbIndex); result = jedis.zcard(key); pool.returnResource(jedis); } catch (Throwable e) { pool.returnBrokenResource(jedis); throw new RuntimeException(e); } return result.intValue(); } @Override public long getExpiredSessions() { log.trace("EXEC getExpiredSessions();"); return 0; } @Override public void setExpiredSessions(long expiredSessions) { log.trace(String.format("EXEC setExpiredSessions(%d);", expiredSessions)); } @Override public int getRejectedSessions() { log.trace("EXEC getRejectedSessions();"); return 0; } @Override public int getSessionMaxAliveTime() { log.trace("EXEC getSessionMaxAliveTime();"); return 0; } @Override public void setSessionMaxAliveTime(int sessionMaxAliveTime) { log.trace(String.format("EXEC setSessionMaxAliveTime(%d);", sessionMaxAliveTime)); } @Override public int getSessionAverageAliveTime() { log.trace("EXEC getSessionAverageAliveTime();"); return 0; } @Override public int getSessionCreateRate() { log.trace("EXEC getSessionCreateRate();"); return 0; } @Override public int getSessionExpireRate() { log.trace("EXEC getSessionExpireRate();"); return 0; } @Override public void add(Session session) { log.trace(String.format("EXEC add(%s);", session)); } @Override public void addPropertyChangeListener(PropertyChangeListener listener) { log.trace(String.format("EXEC addPropertyChangeListener(%s);", listener)); support.addPropertyChangeListener(listener); } @Override public void changeSessionId(Session session) { log.trace(String.format("EXEC changeSessionId(%s);", session)); String oldId = session.getIdInternal(); session.setId(generateSessionId(), false); String newId = session.getIdInternal(); container.fireContainerEvent(Context.CHANGE_SESSION_ID_EVENT, new String[] { oldId, newId }); } @Override public Session createEmptySession() { log.trace("EXEC createEmptySession();"); throw new UnsupportedOperationException("Cannot create empty session."); } @Override public Session createSession(String sessionId) { log.trace(String.format("EXEC createSession(%s);", sessionId)); if ((maxActiveSessions >= 0) && (getActiveSessions() >= maxActiveSessions)) { throw new IllegalStateException(sm.getString("managerBase.createSession.ise")); } String id = sessionId; if (id == null) { id = generateSessionId(); } return new RedisHttpSession(id, this, maxInactiveInterval); } @Override public Session findSession(String id) throws IOException { log.trace(String.format("EXEC findSession(%s);", id)); if (id == null) { return null; } String key = RedisSessionKeys.getLastAccessTimeKey(id); String lastAccessTime; Jedis jedis = pool.getResource(); try { jedis.select(dbIndex); lastAccessTime = jedis.get(key); pool.returnResource(jedis); } catch (Throwable e) { pool.returnBrokenResource(jedis); throw new RuntimeException(e); } if (lastAccessTime == null) { return null; } return new RedisHttpSession(id, this); } @Override public Session[] findSessions() { log.trace("EXEC findSessions();"); Set<String> sessionIds; Jedis jedis = pool.getResource(); try { jedis.select(dbIndex); sessionIds = jedis.zrangeByScore(RedisSessionKeys.getSessionsKey(), 0, Double.MAX_VALUE); pool.returnResource(jedis); } catch (Throwable e) { pool.returnBrokenResource(jedis); throw new RuntimeException(e); } Set<RedisHttpSession> result = new HashSet<RedisHttpSession>(sessionIds.size()); for (String sessionId : sessionIds) { RedisHttpSession session = new RedisHttpSession(sessionId, this); if (session.isValid()) { result.add(session); } } //noinspection ToArrayCallWithZeroLengthArrayArgument return result.toArray(new Session[] {}); } @Override public void load() throws ClassNotFoundException, IOException { log.trace("EXEC load();"); } @Override public void remove(Session session) { log.trace(String.format("EXEC remove(%s);", session)); remove(session, false); } @Override public void remove(Session session, boolean update) { log.trace(String.format("EXEC remove(%s, %s);", session, update)); } @Override public void removePropertyChangeListener(PropertyChangeListener listener) { log.trace(String.format("EXEC removePropertyChangeListener(%s);", listener)); support.removePropertyChangeListener(listener); } @Override public void unload() throws IOException { log.trace("EXEC unload();"); } @Override public void backgroundProcess() { log.trace("EXEC backgroundProcess();"); long max = System.currentTimeMillis() - (maxInactiveInterval * 1000); Set<String> sessionIds; Jedis jedis = pool.getResource(); try { jedis.select(dbIndex); sessionIds = jedis.zrangeByScore(RedisSessionKeys.getSessionsKey(), 0, max); pool.returnResource(jedis); } catch (Throwable e) { pool.returnBrokenResource(jedis); throw new RuntimeException(e); } log.debug(sessionIds.size() + " sessions expired."); Set<String> removedIds = new HashSet<String>(); if (!sessionIds.isEmpty()) { jedis = pool.getResource(); try { for (String sessionId : sessionIds) { if (jedis.zrem(RedisSessionKeys.getSessionsKey(), sessionId) > 0) { removedIds.add(sessionId); } } pool.returnResource(jedis); } catch (Throwable e) { pool.returnBrokenResource(jedis); throw new RuntimeException(e); } for (String sessionId : removedIds) { RedisHttpSession session = new RedisHttpSession(sessionId, this); session.expire(); } } } @Override protected String getDomainInternal() { log.trace("EXEC getDomainInternal();"); return MBeanUtils.getDomain(container); } @Override protected String getObjectNameKeyProperties() { log.trace("EXEC getObjectNameKeyProperties();"); StringBuilder name = new StringBuilder("type=Manager"); if (container instanceof Context) { name.append(",context="); String contextName = container.getName(); if (!contextName.startsWith("/")) { name.append('/'); } name.append(contextName); Context context = (Context) container; name.append(",host="); name.append(context.getParent().getName()); } else { // Unlikely / impossible? Handle it to be safe name.append(",container="); name.append(container.getName()); } return name.toString(); } public String getJedisJndiName() { return jedisJndiName; } public void setJedisJndiName(String jedisJndiName) { this.jedisJndiName = jedisJndiName; } public String getRedisHostname() { return redisHostname; } public void setRedisHostname(String redisHostname) { this.redisHostname = redisHostname; } public int getRedisPort() { return redisPort; } public void setRedisPort(int redisPort) { this.redisPort = redisPort; } public int getRedisTimeout() { return redisTimeout; } public void setRedisTimeout(int redisTimeout) { this.redisTimeout = redisTimeout; } public String getRedisPassword() { return redisPassword; } public void setRedisPassword(String redisPassword) { this.redisPassword = redisPassword; } public JedisPool getPool() { return pool; } public boolean isDisableListeners() { return disableListeners; } public void setDisableListeners(boolean disableListeners) { this.disableListeners = disableListeners; } public int getDbIndex() { return dbIndex; } public void setDbIndex(int dbIndex) { this.dbIndex = dbIndex; } @Override protected void startInternal() throws LifecycleException { log.trace("EXEC startInternal();"); sessionIdGenerator = new SessionIdGenerator(); sessionIdGenerator.setJvmRoute(getJvmRoute()); sessionIdGenerator.setSecureRandomAlgorithm(getSecureRandomAlgorithm()); sessionIdGenerator.setSecureRandomClass(getSecureRandomClass()); sessionIdGenerator.setSecureRandomProvider(getSecureRandomProvider()); sessionIdGenerator.setSessionIdLength(getSessionIdLength()); // Force initialization of the random number generator if (log.isDebugEnabled()) { log.debug("Force random number initialization starting"); } sessionIdGenerator.generateSessionId(); if (log.isDebugEnabled()) { log.debug("Force random number initialization completed"); } log.debug("Trying to get jedis pool from JNDI..."); InitialContext initialContext = null; try { initialContext = new InitialContext(); pool = (JedisPool) initialContext.lookup("java:/comp/env/" + jedisJndiName); } catch (NamingException e) { log.warn("JedisPool not found in JNDI"); } log.debug("Pool from JNDI: " + pool); if (pool == null) { GenericObjectPool.Config config = new GenericObjectPool.Config(); config.testOnBorrow = true; pool = new JedisPool(config, redisHostname, redisPort, redisTimeout, redisPassword); } if (!disableListeners) { Executors.newSingleThreadExecutor().execute(eventListenerThread); } setState(LifecycleState.STARTING); } public Engine getEngine() { Engine e = null; for (Container c = getContainer(); e == null && c != null; c = c.getParent()) { if (c instanceof Engine) { e = (Engine) c; } } return e; } public String getJvmRoute() { Engine e = getEngine(); return e == null ? null : e.getJvmRoute(); } public String getSecureRandomClass() { return this.secureRandomClass; } public void setSecureRandomClass(String secureRandomClass) { String oldSecureRandomClass = this.secureRandomClass; this.secureRandomClass = secureRandomClass; support.firePropertyChange("secureRandomClass", oldSecureRandomClass, this.secureRandomClass); } public String getSecureRandomAlgorithm() { return secureRandomAlgorithm; } public void setSecureRandomAlgorithm(String secureRandomAlgorithm) { this.secureRandomAlgorithm = secureRandomAlgorithm; } public String getSecureRandomProvider() { return secureRandomProvider; } public void setSecureRandomProvider(String secureRandomProvider) { this.secureRandomProvider = secureRandomProvider; } @Override protected void stopInternal() throws LifecycleException { log.trace("EXEC stopInternal();"); setState(LifecycleState.STOPPING); this.sessionIdGenerator = null; if (!disableListeners) { eventListenerThread.stop(); } pool.destroy(); } protected String generateSessionId() { String result; try { do { result = sessionIdGenerator.generateSessionId(); } while (findSession(result) != null); } catch (IOException e) { throw new RuntimeException(e); } return result; } @Override public void propertyChange(PropertyChangeEvent event) { log.trace(String.format("EXEC propertyChange(%s);", event)); if (!(event.getSource() instanceof Context)) { return; } if (event.getPropertyName().equals("sessionTimeout")) { try { setMaxInactiveInterval((Integer) event.getNewValue() * 60); } catch (NumberFormatException e) { log.error(sm.getString("managerBase.sessionTimeout", event.getNewValue())); } } } }