Tomcat clustering with Redis data-cache implementation version-2 changes initial commit..
parent
eac72a087e
commit
4ede7bfca5
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<classpath>
|
||||||
|
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="optional" value="true"/>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="optional" value="true"/>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||||
|
<attributes>
|
||||||
|
<attribute name="maven.pomderived" value="true"/>
|
||||||
|
</attributes>
|
||||||
|
</classpathentry>
|
||||||
|
<classpathentry kind="output" path="target/classes"/>
|
||||||
|
</classpath>
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Compiled class file
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Log file
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# BlueJ files
|
||||||
|
*.ctxt
|
||||||
|
|
||||||
|
# Mobile Tools for Java (J2ME)
|
||||||
|
.mtj.tmp/
|
||||||
|
|
||||||
|
# Package Files #
|
||||||
|
*.jar
|
||||||
|
*.war
|
||||||
|
*.ear
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
|
||||||
|
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
|
||||||
|
hs_err_pid*
|
4
.project
4
.project
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<projectDescription>
|
<projectDescription>
|
||||||
<name>TomcatRedisSessionManager</name>
|
<name>tomcat-cluster-redis-session-manager</name>
|
||||||
<comment></comment>
|
<comment></comment>
|
||||||
<projects>
|
<projects>
|
||||||
</projects>
|
</projects>
|
||||||
|
@ -22,8 +22,8 @@
|
||||||
</buildCommand>
|
</buildCommand>
|
||||||
</buildSpec>
|
</buildSpec>
|
||||||
<natures>
|
<natures>
|
||||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
|
||||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||||
|
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||||
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
|
<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
|
||||||
</natures>
|
</natures>
|
||||||
</projectDescription>
|
</projectDescription>
|
||||||
|
|
77
pom.xml
77
pom.xml
|
@ -1,37 +1,26 @@
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>TomcatClusterEnabledRedisSessionManager</groupId>
|
|
||||||
<artifactId>TomcatClusterEnabledRedisSessionManager</artifactId>
|
<groupId>tomcat-cluster-redis-session-manager</groupId>
|
||||||
<version>1.0</version>
|
<artifactId>tomcat-cluster-redis-session-manager</artifactId>
|
||||||
<name>TomcatClusterEnabledRedisSessionManager</name>
|
<version>2.0</version>
|
||||||
<description>Tomcat 7 cluster enabled redis session manager. it supports Redis both single master and cluster</description>
|
<packaging>jar</packaging>
|
||||||
<build>
|
|
||||||
<sourceDirectory>src</sourceDirectory>
|
<name>tomcat-cluster-redis-session-manager</name>
|
||||||
<resources>
|
<url>http://maven.apache.org</url>
|
||||||
<resource>
|
|
||||||
<directory>resources</directory>
|
<properties>
|
||||||
<excludes>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<exclude>**/*.java</exclude>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
</excludes>
|
<java.version>1.7</java.version>
|
||||||
</resource>
|
</properties>
|
||||||
</resources>
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
|
||||||
<version>3.1</version>
|
|
||||||
<configuration>
|
|
||||||
<source>1.7</source>
|
|
||||||
<target>1.7</target>
|
|
||||||
</configuration>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</build>
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>redis.clients</groupId>
|
<groupId>redis.clients</groupId>
|
||||||
<artifactId>jedis</artifactId>
|
<artifactId>jedis</artifactId>
|
||||||
<version>2.8.0</version>
|
<version>2.9.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
|
@ -43,5 +32,37 @@
|
||||||
<artifactId>commons-logging</artifactId>
|
<artifactId>commons-logging</artifactId>
|
||||||
<version>1.2</version>
|
<version>1.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- For local development -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>apache-tomcat</groupId>
|
||||||
|
<artifactId>catalina</artifactId>
|
||||||
|
<version>apache-tomcat-8.5.16</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>apache-tomcat</groupId>
|
||||||
|
<artifactId>servlet-api</artifactId>
|
||||||
|
<version>apache-tomcat-8.5.16</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>apache-tomcat</groupId>
|
||||||
|
<artifactId>tomcat-api</artifactId>
|
||||||
|
<version>apache-tomcat-8.5.16</version>
|
||||||
|
</dependency>
|
||||||
|
<!-- For local development end.. -->
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.6.1</version>
|
||||||
|
<configuration>
|
||||||
|
<source>1.7</source>
|
||||||
|
<target>1.7</target>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
/**
|
|
||||||
* Tomcat clustering implementation
|
|
||||||
*
|
|
||||||
* Redis session manager is the pluggable one. It uses to store session objects from Tomcat catalina to Redis data cache.
|
|
||||||
*
|
|
||||||
* @author Ranjith Manickam
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
Supports:
|
|
||||||
* Apache Tomcat 8
|
|
||||||
|
|
||||||
Downloads:
|
|
||||||
Pre-requisite:
|
|
||||||
1. jedis-2.8.0.jar
|
|
||||||
2. commons-pool2-2.4.2.jar
|
|
||||||
3. commons-logging-1.2.jar
|
|
||||||
|
|
||||||
Tomcat Redis Cluster Enabled Session Manager jar is available in below location:
|
|
||||||
https://github.com/ran-jit/TomcatClusterRedisSessionManager/wiki
|
|
||||||
|
|
||||||
Steps to be done,
|
|
||||||
1. Move the downloaded jars to tomcat/lib directory
|
|
||||||
* $catalina.home/lib/
|
|
||||||
|
|
||||||
2. Add tomcat system property "catalina.base"
|
|
||||||
* catalina.base="TOMCAT_LOCATION"
|
|
||||||
|
|
||||||
3. Extract downloaded jar (TomcatClusterEnabledRedisSessionManager-1.0.jar) to configure Redis credentials in RedisDataCache.properties file and move the file to tomcat/conf directory
|
|
||||||
* tomcat/conf/RedisDataCache.properties
|
|
||||||
|
|
||||||
4. Add the below two lines in tomcat/conf/context.xml
|
|
||||||
* <Valve className="com.r.tomcat.session.management.RequestSessionHandlerValve">
|
|
||||||
* <Manager className="com.r.tomcat.session.management.RequestSessionManager">
|
|
||||||
|
|
||||||
5. Verify the session expiration time in tomcat/conf/web.xml
|
|
||||||
* <session-config>
|
|
||||||
* <session-timeout>60<session-timeout>
|
|
||||||
* <session-config>
|
|
||||||
|
|
||||||
Note:
|
|
||||||
* The Redis session manager supports, both single redis master and redis cluster based on the redis.properties configuration.
|
|
|
@ -1,14 +0,0 @@
|
||||||
# redis hosts ex: 127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.2:6380, ....
|
|
||||||
redis.hosts=127.0.0.1:6379
|
|
||||||
|
|
||||||
# Redis Password
|
|
||||||
redis.password=
|
|
||||||
|
|
||||||
# set true to enable redis cluster mode
|
|
||||||
redis.cluster.enabled=false
|
|
||||||
|
|
||||||
# Redis database (default 0)
|
|
||||||
#redis.database=0
|
|
||||||
|
|
||||||
# Redis connection timeout (default 2000)
|
|
||||||
#redis.timeout=2000
|
|
|
@ -1,24 +0,0 @@
|
||||||
package com.r.tomcat.session.data.cache;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tomcat clustering implementation
|
|
||||||
*
|
|
||||||
* This interface holds cache implementation
|
|
||||||
*
|
|
||||||
* @author Ranjith Manickam
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
|
||||||
public interface IRequestSessionCacheUtils
|
|
||||||
{
|
|
||||||
public boolean isAvailable();
|
|
||||||
|
|
||||||
public void setByteArray(String key, byte[] value);
|
|
||||||
|
|
||||||
public byte[] getByteArray(String key);
|
|
||||||
|
|
||||||
public void deleteKey(String key);
|
|
||||||
|
|
||||||
public Long setStringIfKeyNotExists(byte[] key, byte[] value);
|
|
||||||
|
|
||||||
public void expire(String key, int ttl);
|
|
||||||
}
|
|
|
@ -1,155 +0,0 @@
|
||||||
package com.r.tomcat.session.data.cache;
|
|
||||||
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
|
||||||
import org.apache.commons.logging.LogFactory;
|
|
||||||
|
|
||||||
import redis.clients.jedis.Jedis;
|
|
||||||
import redis.clients.jedis.exceptions.JedisConnectionException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tomcat clustering implementation
|
|
||||||
*
|
|
||||||
* This cache util is uses to store and retrieve the session object in Redis data cache non cluster
|
|
||||||
*
|
|
||||||
* @author Ranjith Manickam
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
|
||||||
public class RedisCacheUtil implements IRequestSessionCacheUtils
|
|
||||||
{
|
|
||||||
private Log log = LogFactory.getLog(RedisCacheUtil.class);
|
|
||||||
|
|
||||||
public boolean available = false;
|
|
||||||
|
|
||||||
private static int numRetries = 3;
|
|
||||||
|
|
||||||
private RedisManager manager = null;
|
|
||||||
|
|
||||||
RedisCacheUtil(Properties properties) throws Exception {
|
|
||||||
try {
|
|
||||||
manager = RedisManager.createInstance(properties);
|
|
||||||
} catch (Exception e) {
|
|
||||||
this.available = false;
|
|
||||||
log.error("Exception initializing Redis: ", e);
|
|
||||||
}
|
|
||||||
this.available = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAvailable() {
|
|
||||||
return available;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setByteArray(String key, byte[] value) {
|
|
||||||
int tries = 0;
|
|
||||||
boolean sucess = false;
|
|
||||||
do {
|
|
||||||
tries++;
|
|
||||||
try {
|
|
||||||
if (key != null && value != null) {
|
|
||||||
Jedis jedis = manager.getJedis();
|
|
||||||
jedis.set(key.getBytes(), value);
|
|
||||||
jedis.close();
|
|
||||||
}
|
|
||||||
sucess = true;
|
|
||||||
} catch (JedisConnectionException e) {
|
|
||||||
log.error("Jedis connection failed, retrying..." + tries);
|
|
||||||
if (tries == numRetries) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (!sucess && tries <= numRetries);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Long setStringIfKeyNotExists(byte[] key, byte[] value) {
|
|
||||||
int tries = 0;
|
|
||||||
Long retVal = null;
|
|
||||||
boolean sucess = false;
|
|
||||||
do {
|
|
||||||
tries++;
|
|
||||||
try {
|
|
||||||
if (key != null && value != null) {
|
|
||||||
Jedis jedis = manager.getJedis();
|
|
||||||
retVal = jedis.setnx(key, value);
|
|
||||||
jedis.close();
|
|
||||||
}
|
|
||||||
sucess = true;
|
|
||||||
} catch (JedisConnectionException e) {
|
|
||||||
log.error("Jedis connection failed, retrying..." + tries);
|
|
||||||
if (tries == numRetries) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (!sucess && tries <= numRetries);
|
|
||||||
return retVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void expire(String key, int ttl) {
|
|
||||||
int tries = 0;
|
|
||||||
boolean sucess = false;
|
|
||||||
do {
|
|
||||||
tries++;
|
|
||||||
try {
|
|
||||||
Jedis jedis = manager.getJedis();
|
|
||||||
jedis.expire(key, ttl);
|
|
||||||
jedis.close();
|
|
||||||
sucess = true;
|
|
||||||
} catch (JedisConnectionException e) {
|
|
||||||
log.error("Jedis connection failed, retrying..." + tries);
|
|
||||||
if (tries == numRetries) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (!sucess && tries <= numRetries);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getByteArray(String key) {
|
|
||||||
int tries = 0;
|
|
||||||
boolean sucess = false;
|
|
||||||
byte[] array = new byte[1];
|
|
||||||
do {
|
|
||||||
tries++;
|
|
||||||
try {
|
|
||||||
if (key != null) {
|
|
||||||
Jedis jedis = manager.getJedis();
|
|
||||||
array = jedis.get(key.getBytes());
|
|
||||||
jedis.close();
|
|
||||||
}
|
|
||||||
sucess = true;
|
|
||||||
} catch (JedisConnectionException e) {
|
|
||||||
log.error("Jedis connection failed, retrying..." + tries);
|
|
||||||
if (tries == numRetries) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (!sucess && tries <= numRetries);
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deleteKey(String key) {
|
|
||||||
int tries = 0;
|
|
||||||
boolean sucess = false;
|
|
||||||
do {
|
|
||||||
tries++;
|
|
||||||
try {
|
|
||||||
if (key != null) {
|
|
||||||
Jedis jedis = manager.getJedis();
|
|
||||||
jedis.del(key);
|
|
||||||
jedis.close();
|
|
||||||
}
|
|
||||||
sucess = true;
|
|
||||||
} catch (JedisConnectionException e) {
|
|
||||||
log.error("Jedis connection failed, retrying..." + tries);
|
|
||||||
if (tries == numRetries) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} while (!sucess && tries <= numRetries);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
package com.r.tomcat.session.data.cache;
|
|
||||||
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
|
||||||
import org.apache.commons.logging.LogFactory;
|
|
||||||
|
|
||||||
import redis.clients.jedis.exceptions.JedisClusterMaxRedirectionsException;
|
|
||||||
import redis.clients.jedis.exceptions.JedisConnectionException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tomcat clustering implementation
|
|
||||||
*
|
|
||||||
* This cache util is uses to store and retrieve the session object in Redis data cache cluster
|
|
||||||
*
|
|
||||||
* @author Ranjith Manickam
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
|
||||||
public class RedisClusterCacheUtil implements IRequestSessionCacheUtils
|
|
||||||
{
|
|
||||||
private Log log = LogFactory.getLog(RedisClusterCacheUtil.class);
|
|
||||||
|
|
||||||
public boolean available = false;
|
|
||||||
|
|
||||||
private static int numRetries = 30;
|
|
||||||
|
|
||||||
private RedisClusterManager clusterManager = null;
|
|
||||||
|
|
||||||
RedisClusterCacheUtil(Properties properties) throws Exception {
|
|
||||||
try {
|
|
||||||
clusterManager = RedisClusterManager.createInstance(properties);
|
|
||||||
} catch (Exception e) {
|
|
||||||
this.available = false;
|
|
||||||
log.error("Exception initializing Redis cluster: " + e);
|
|
||||||
}
|
|
||||||
this.available = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAvailable() {
|
|
||||||
return available;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setByteArray(String key, byte[] value) {
|
|
||||||
int tries = 0;
|
|
||||||
boolean sucess = false;
|
|
||||||
do {
|
|
||||||
tries++;
|
|
||||||
try {
|
|
||||||
if (key != null && value != null) {
|
|
||||||
clusterManager.getJedis().set(key.getBytes(), value);
|
|
||||||
}
|
|
||||||
sucess = true;
|
|
||||||
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException e) {
|
|
||||||
log.error("Jedis connection failed, retrying..." + tries);
|
|
||||||
if (tries == numRetries) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
waitforFailover();
|
|
||||||
}
|
|
||||||
} while (!sucess && tries <= numRetries);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Long setStringIfKeyNotExists(byte[] key, byte[] value) {
|
|
||||||
int tries = 0;
|
|
||||||
Long retVal = null;
|
|
||||||
boolean sucess = false;
|
|
||||||
do {
|
|
||||||
tries++;
|
|
||||||
try {
|
|
||||||
if (key != null && value != null) {
|
|
||||||
retVal = clusterManager.getJedis().setnx(key, value);
|
|
||||||
}
|
|
||||||
sucess = true;
|
|
||||||
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException e) {
|
|
||||||
log.error("Jedis connection failed, retrying..." + tries);
|
|
||||||
if (tries == numRetries) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
waitforFailover();
|
|
||||||
}
|
|
||||||
} while (!sucess && tries <= numRetries);
|
|
||||||
return retVal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void expire(String key, int ttl) {
|
|
||||||
int tries = 0;
|
|
||||||
boolean sucess = false;
|
|
||||||
do {
|
|
||||||
tries++;
|
|
||||||
try {
|
|
||||||
clusterManager.getJedis().expire(key, ttl);
|
|
||||||
sucess = true;
|
|
||||||
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException e) {
|
|
||||||
log.error("Jedis connection failed, retrying..." + tries);
|
|
||||||
if (tries == numRetries) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
waitforFailover();
|
|
||||||
}
|
|
||||||
} while (!sucess && tries <= numRetries);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getByteArray(String key) {
|
|
||||||
int tries = 0;
|
|
||||||
boolean sucess = false;
|
|
||||||
byte[] array = new byte[1];
|
|
||||||
do {
|
|
||||||
tries++;
|
|
||||||
try {
|
|
||||||
if (key != null) {
|
|
||||||
array = clusterManager.getJedis().get(key.getBytes());
|
|
||||||
}
|
|
||||||
sucess = true;
|
|
||||||
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException e) {
|
|
||||||
log.error("Jedis connection failed, retrying..." + tries);
|
|
||||||
if (tries == numRetries) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
waitforFailover();
|
|
||||||
}
|
|
||||||
} while (!sucess && tries <= numRetries);
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void deleteKey(String key) {
|
|
||||||
int tries = 0;
|
|
||||||
boolean sucess = false;
|
|
||||||
do {
|
|
||||||
tries++;
|
|
||||||
try {
|
|
||||||
if (key != null) {
|
|
||||||
clusterManager.getJedis().del(key);
|
|
||||||
}
|
|
||||||
sucess = true;
|
|
||||||
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException e) {
|
|
||||||
log.error("Jedis connection failed, retrying..." + tries);
|
|
||||||
if (tries == numRetries) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
waitforFailover();
|
|
||||||
}
|
|
||||||
} while (!sucess && tries <= numRetries);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void waitforFailover() {
|
|
||||||
try {
|
|
||||||
Thread.sleep(4000);
|
|
||||||
} catch (InterruptedException ex) {
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
package com.r.tomcat.session.data.cache;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import redis.clients.jedis.HostAndPort;
|
|
||||||
import redis.clients.jedis.JedisCluster;
|
|
||||||
import redis.clients.jedis.JedisPoolConfig;
|
|
||||||
import redis.clients.jedis.Protocol;
|
|
||||||
|
|
||||||
import com.r.tomcat.session.data.cache.constants.RedisConstants;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tomcat clustering implementation
|
|
||||||
*
|
|
||||||
* This manager is uses to initialize the Redis data cache cluster client
|
|
||||||
*
|
|
||||||
* @author Ranjith Manickam
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
|
||||||
public class RedisClusterManager
|
|
||||||
{
|
|
||||||
private Properties properties;
|
|
||||||
|
|
||||||
private static JedisCluster jedisCluster;
|
|
||||||
|
|
||||||
private static RedisClusterManager instance;
|
|
||||||
|
|
||||||
private RedisClusterManager(Properties properties) {
|
|
||||||
this.properties = properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RedisClusterManager createInstance(Properties properties) {
|
|
||||||
instance = new RedisClusterManager(properties);
|
|
||||||
instance.connect();
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final static RedisClusterManager getInstance() throws Exception {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void connect() {
|
|
||||||
JedisPoolConfig poolConfig = new JedisPoolConfig();
|
|
||||||
int maxActive = Integer.parseInt(properties.getProperty(RedisConstants.MAX_ACTIVE, RedisConstants.DEFAULT_MAX_ACTIVE_VALUE));
|
|
||||||
poolConfig.setMaxTotal(maxActive);
|
|
||||||
boolean testOnBorrow = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_ONBORROW, RedisConstants.DEFAULT_TEST_ONBORROW_VALUE));
|
|
||||||
poolConfig.setTestOnBorrow(testOnBorrow);
|
|
||||||
boolean testOnReturn = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_ONRETURN, RedisConstants.DEFAULT_TEST_ONRETURN_VALUE));
|
|
||||||
poolConfig.setTestOnReturn(testOnReturn);
|
|
||||||
int maxIdle = Integer.parseInt(properties.getProperty(RedisConstants.MAX_IDLE, RedisConstants.DEFAULT_MAX_ACTIVE_VALUE));
|
|
||||||
poolConfig.setMaxIdle(maxIdle);
|
|
||||||
int minIdle = Integer.parseInt(properties.getProperty(RedisConstants.MIN_IDLE, RedisConstants.DEFAULT_MIN_IDLE_VALUE));
|
|
||||||
poolConfig.setMinIdle(minIdle);
|
|
||||||
boolean testWhileIdle = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_WHILEIDLE, RedisConstants.DEFAULT_TEST_WHILEIDLE_VALUE));
|
|
||||||
poolConfig.setTestWhileIdle(testWhileIdle);
|
|
||||||
int testNumPerEviction = Integer.parseInt(properties.getProperty(RedisConstants.TEST_NUMPEREVICTION, RedisConstants.DEFAULT_TEST_NUMPEREVICTION_VALUE));
|
|
||||||
poolConfig.setNumTestsPerEvictionRun(testNumPerEviction);
|
|
||||||
long timeBetweenEviction = Long.parseLong(properties.getProperty(RedisConstants.TIME_BETWEENEVICTION, RedisConstants.DEFAULT_TIME_BETWEENEVICTION_VALUE));
|
|
||||||
poolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEviction);
|
|
||||||
int timeout = Integer.parseInt(properties.getProperty(RedisConstants.TIMEOUT, String.valueOf(Protocol.DEFAULT_TIMEOUT)));
|
|
||||||
jedisCluster = new JedisCluster(getJedisClusterNodesSet(properties.getProperty(RedisConstants.HOSTS, Protocol.DEFAULT_HOST.concat(":").concat(String.valueOf(Protocol.DEFAULT_PORT)))), (timeout < Protocol.DEFAULT_TIMEOUT ? Protocol.DEFAULT_TIMEOUT : timeout), poolConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* method to get the cluster nodes
|
|
||||||
*
|
|
||||||
* @param hosts
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private Set<HostAndPort> getJedisClusterNodesSet(String hosts) {
|
|
||||||
Set<HostAndPort> nodes = new HashSet<HostAndPort>();
|
|
||||||
hosts = hosts.replaceAll("\\s", "");
|
|
||||||
String[] hostPorts = hosts.split(",");
|
|
||||||
for (String hostPort : hostPorts) {
|
|
||||||
String[] hostPortArr = hostPort.split(":");
|
|
||||||
nodes.add(new HostAndPort(hostPortArr[0], Integer.valueOf(hostPortArr[1])));
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JedisCluster getJedis() {
|
|
||||||
return jedisCluster;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
package com.r.tomcat.session.data.cache;
|
|
||||||
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
import redis.clients.jedis.Jedis;
|
|
||||||
import redis.clients.jedis.JedisPool;
|
|
||||||
import redis.clients.jedis.JedisPoolConfig;
|
|
||||||
import redis.clients.jedis.Protocol;
|
|
||||||
|
|
||||||
import com.r.tomcat.session.data.cache.constants.RedisConstants;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tomcat clustering implementation
|
|
||||||
*
|
|
||||||
* This manager is uses to initialize the Redis data cache client
|
|
||||||
*
|
|
||||||
* @author Ranjith Manickam
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
|
||||||
public class RedisManager
|
|
||||||
{
|
|
||||||
private static RedisManager instance;
|
|
||||||
|
|
||||||
private static JedisPool pool;
|
|
||||||
|
|
||||||
private Properties properties;
|
|
||||||
|
|
||||||
private RedisManager(Properties properties) {
|
|
||||||
this.properties = properties;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RedisManager createInstance(Properties properties) {
|
|
||||||
instance = new RedisManager(properties);
|
|
||||||
instance.connect();
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public final static RedisManager getInstance() throws Exception {
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void connect() {
|
|
||||||
JedisPoolConfig poolConfig = new JedisPoolConfig();
|
|
||||||
int maxActive = Integer.parseInt(properties.getProperty(RedisConstants.MAX_ACTIVE, RedisConstants.DEFAULT_MAX_ACTIVE_VALUE));
|
|
||||||
poolConfig.setMaxTotal(maxActive);
|
|
||||||
boolean testOnBorrow = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_ONBORROW, RedisConstants.DEFAULT_TEST_ONBORROW_VALUE));
|
|
||||||
poolConfig.setTestOnBorrow(testOnBorrow);
|
|
||||||
boolean testOnReturn = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_ONRETURN, RedisConstants.DEFAULT_TEST_ONRETURN_VALUE));
|
|
||||||
poolConfig.setTestOnReturn(testOnReturn);
|
|
||||||
int maxIdle = Integer.parseInt(properties.getProperty(RedisConstants.MAX_ACTIVE, RedisConstants.DEFAULT_MAX_ACTIVE_VALUE));
|
|
||||||
poolConfig.setMaxIdle(maxIdle);
|
|
||||||
int minIdle = Integer.parseInt(properties.getProperty(RedisConstants.MIN_IDLE, RedisConstants.DEFAULT_MIN_IDLE_VALUE));
|
|
||||||
poolConfig.setMinIdle(minIdle);
|
|
||||||
boolean testWhileIdle = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_WHILEIDLE, RedisConstants.DEFAULT_TEST_WHILEIDLE_VALUE));
|
|
||||||
poolConfig.setTestWhileIdle(testWhileIdle);
|
|
||||||
int testNumPerEviction = Integer.parseInt(properties.getProperty(RedisConstants.TEST_NUMPEREVICTION, RedisConstants.DEFAULT_TEST_NUMPEREVICTION_VALUE));
|
|
||||||
poolConfig.setNumTestsPerEvictionRun(testNumPerEviction);
|
|
||||||
long timeBetweenEviction = Long.parseLong(properties.getProperty(RedisConstants.TIME_BETWEENEVICTION, RedisConstants.DEFAULT_TIME_BETWEENEVICTION_VALUE));
|
|
||||||
poolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEviction);
|
|
||||||
String hosts = properties.getProperty(RedisConstants.HOSTS, Protocol.DEFAULT_HOST.concat(":").concat(String.valueOf(Protocol.DEFAULT_PORT)));
|
|
||||||
String host = null;
|
|
||||||
int port = 0;
|
|
||||||
hosts = hosts.replaceAll("\\s", "");
|
|
||||||
String[] hostPorts = hosts.split(",");
|
|
||||||
for (String hostPort : hostPorts) {
|
|
||||||
String[] hostPortArr = hostPort.split(":");
|
|
||||||
host = hostPortArr[0];
|
|
||||||
port = Integer.valueOf(hostPortArr[1]);
|
|
||||||
if (!host.isEmpty() && port != 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String password = properties.getProperty(RedisConstants.PASSWORD);
|
|
||||||
int database = Integer.parseInt(properties.getProperty(RedisConstants.DATABASE, String.valueOf(Protocol.DEFAULT_DATABASE)));
|
|
||||||
int timeout = Integer.parseInt(properties.getProperty(RedisConstants.TIMEOUT, String.valueOf(Protocol.DEFAULT_TIMEOUT)));
|
|
||||||
pool = new JedisPool(poolConfig, host, port, (timeout < Protocol.DEFAULT_TIMEOUT ? Protocol.DEFAULT_TIMEOUT : timeout), ((password == null || password == "" || password.isEmpty()) ? null : password), database);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Jedis getJedis() {
|
|
||||||
return pool.getResource();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
package com.r.tomcat.session.data.cache;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
|
||||||
import org.apache.commons.logging.LogFactory;
|
|
||||||
|
|
||||||
import com.r.tomcat.session.data.cache.constants.RedisConstants;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tomcat clustering implementation
|
|
||||||
*
|
|
||||||
* This factory is uses to get the request session data cache
|
|
||||||
*
|
|
||||||
* @author Ranjith Manickam
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
|
||||||
public class RequestSessionCacheFactory
|
|
||||||
{
|
|
||||||
private static Log log = LogFactory.getLog(RequestSessionCacheFactory.class);
|
|
||||||
|
|
||||||
protected static IRequestSessionCacheUtils requestCache;
|
|
||||||
|
|
||||||
public static synchronized IRequestSessionCacheUtils getInstance() {
|
|
||||||
try {
|
|
||||||
if (requestCache == null) {
|
|
||||||
Properties properties = getRedisProperties();
|
|
||||||
if (!Boolean.valueOf(properties.getProperty(RedisConstants.IS_CLUSTER_ENABLED, RedisConstants.DEFAULT_IS_CLUSTER_ENABLED))) {
|
|
||||||
requestCache = new RedisCacheUtil(properties);
|
|
||||||
} else {
|
|
||||||
requestCache = new RedisClusterCacheUtil(properties);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error occurred initializing redis", e);
|
|
||||||
}
|
|
||||||
return requestCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Properties getRedisProperties() throws Exception {
|
|
||||||
Properties properties = null;
|
|
||||||
try {
|
|
||||||
if (properties == null || properties.isEmpty()) {
|
|
||||||
InputStream resourceStream = null;
|
|
||||||
try {
|
|
||||||
resourceStream = null;
|
|
||||||
properties = new Properties();
|
|
||||||
File file = new File(System.getProperty("catalina.base").concat(File.separator).concat("conf").concat(File.separator).concat(RedisConstants.REDIS_DATA_CACHE_PROPERTIES_FILE));
|
|
||||||
if (file.exists()) {
|
|
||||||
resourceStream = new FileInputStream(file);
|
|
||||||
}
|
|
||||||
if (resourceStream == null) {
|
|
||||||
ClassLoader loader = Thread.currentThread().getContextClassLoader();
|
|
||||||
resourceStream = loader.getResourceAsStream(RedisConstants.REDIS_DATA_CACHE_PROPERTIES_FILE);
|
|
||||||
}
|
|
||||||
properties.load(resourceStream);
|
|
||||||
} finally {
|
|
||||||
resourceStream.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Error occurred fetching redis informations", e);
|
|
||||||
}
|
|
||||||
return properties;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
package com.r.tomcat.session.management;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.Principal;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
import org.apache.catalina.Manager;
|
|
||||||
import org.apache.catalina.session.StandardSession;
|
|
||||||
import org.apache.commons.logging.Log;
|
|
||||||
import org.apache.commons.logging.LogFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tomcat clustering implementation
|
|
||||||
*
|
|
||||||
* This class is uses to store and retrieve the HTTP request session objects from catalina to data cache
|
|
||||||
*
|
|
||||||
* @author Ranjith Manickam
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
|
||||||
public class CustomRequestSession extends StandardSession
|
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 8237845843135996014L;
|
|
||||||
|
|
||||||
private final Log log = LogFactory.getLog(CustomRequestSession.class);
|
|
||||||
|
|
||||||
protected Boolean dirty;
|
|
||||||
|
|
||||||
protected HashMap<String, Object> changedAttributes;
|
|
||||||
|
|
||||||
protected static Boolean manualDirtyTrackingSupportEnabled = false;
|
|
||||||
|
|
||||||
protected static String manualDirtyTrackingAttributeKey = "__changed__";
|
|
||||||
|
|
||||||
public static void setManualDirtyTrackingSupportEnabled(Boolean enabled) {
|
|
||||||
manualDirtyTrackingSupportEnabled = enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setManualDirtyTrackingAttributeKey(String key) {
|
|
||||||
manualDirtyTrackingAttributeKey = key;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CustomRequestSession(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<>();
|
|
||||||
dirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setAttribute(String key, Object value) {
|
|
||||||
if (manualDirtyTrackingSupportEnabled && manualDirtyTrackingAttributeKey.equals(key)) {
|
|
||||||
dirty = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Object oldValue = getAttribute(key);
|
|
||||||
super.setAttribute(key, value);
|
|
||||||
|
|
||||||
if ((value != null || oldValue != null) && (value == null && oldValue != null || oldValue == null && value != null || !value.getClass().isInstance(oldValue) || !value.equals(oldValue))) {
|
|
||||||
if (this.manager instanceof RequestSessionManager && ((RequestSessionManager) this.manager).getSaveOnChange()) {
|
|
||||||
try {
|
|
||||||
((RequestSessionManager) this.manager).save(this, true);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.error("Error saving session on setAttribute (triggered by saveOnChange=true): " + ex.getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
changedAttributes.put(key, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getAttribute(String name) {
|
|
||||||
return super.getAttribute(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Enumeration<String> getAttributeNames() {
|
|
||||||
return super.getAttributeNames();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeAttribute(String name) {
|
|
||||||
super.removeAttribute(name);
|
|
||||||
if (this.manager instanceof RequestSessionManager && ((RequestSessionManager) this.manager).getSaveOnChange()) {
|
|
||||||
try {
|
|
||||||
((RequestSessionManager) this.manager).save(this, true);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
log.error("Error saving session on removeAttribute (triggered by saveOnChange=true): " + ex.getMessage());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dirty = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setId(String id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setPrincipal(Principal principal) {
|
|
||||||
dirty = true;
|
|
||||||
super.setPrincipal(principal);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeObjectData(java.io.ObjectOutputStream out) throws IOException {
|
|
||||||
super.writeObjectData(out);
|
|
||||||
out.writeLong(this.getCreationTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void readObjectData(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
|
|
||||||
super.readObjectData(in);
|
|
||||||
this.setCreationTime(in.readLong());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
package com.r.tomcat.session.management;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tomcat clustering implementation
|
|
||||||
*
|
|
||||||
* This class is uses to store and retrieve the HTTP request session objects from catalina to data cache
|
|
||||||
*
|
|
||||||
* @author Ranjith Manickam
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
|
||||||
public class DeserializedSessionContainer
|
|
||||||
{
|
|
||||||
public final CustomRequestSession session;
|
|
||||||
|
|
||||||
public final SessionSerializationMetadata metadata;
|
|
||||||
|
|
||||||
public DeserializedSessionContainer(CustomRequestSession session, SessionSerializationMetadata metadata) {
|
|
||||||
this.session = session;
|
|
||||||
this.metadata = metadata;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,348 +0,0 @@
|
||||||
package com.r.tomcat.session.management;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.EnumSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
import org.apache.catalina.Lifecycle;
|
|
||||||
import org.apache.catalina.LifecycleException;
|
|
||||||
import org.apache.catalina.LifecycleListener;
|
|
||||||
import org.apache.catalina.LifecycleState;
|
|
||||||
import org.apache.catalina.Loader;
|
|
||||||
import org.apache.catalina.Session;
|
|
||||||
import org.apache.catalina.Valve;
|
|
||||||
import org.apache.catalina.connector.Request;
|
|
||||||
import org.apache.catalina.session.ManagerBase;
|
|
||||||
import org.apache.catalina.util.LifecycleSupport;
|
|
||||||
import org.apache.commons.logging.Log;
|
|
||||||
import org.apache.commons.logging.LogFactory;
|
|
||||||
|
|
||||||
import com.r.tomcat.session.data.cache.IRequestSessionCacheUtils;
|
|
||||||
import com.r.tomcat.session.data.cache.RequestSessionCacheFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tomcat clustering implementation
|
|
||||||
*
|
|
||||||
* This class is uses to store and retrieve the HTTP request session objects from catalina to data cache
|
|
||||||
*
|
|
||||||
* @author Ranjith Manickam
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
|
||||||
public class RequestSessionManager extends ManagerBase implements Lifecycle
|
|
||||||
{
|
|
||||||
private final Log log = LogFactory.getLog(RequestSessionManager.class);
|
|
||||||
|
|
||||||
private IRequestSessionCacheUtils requestSessionCacheUtils;
|
|
||||||
|
|
||||||
protected SessionDataSerializer serializer;
|
|
||||||
|
|
||||||
protected RequestSessionHandlerValve handlerValve;
|
|
||||||
|
|
||||||
protected byte[] NULL_SESSION = "null".getBytes();
|
|
||||||
|
|
||||||
protected LifecycleSupport lifecycle = new LifecycleSupport(this);
|
|
||||||
|
|
||||||
protected ThreadLocal<String> currentSessionId = new ThreadLocal<>();
|
|
||||||
|
|
||||||
protected ThreadLocal<CustomRequestSession> currentSession = new ThreadLocal<>();
|
|
||||||
|
|
||||||
protected ThreadLocal<Boolean> currentSessionIsPersisted = new ThreadLocal<>();
|
|
||||||
|
|
||||||
protected EnumSet<SessionPersistPolicy> sessionPersistPoliciesSet = EnumSet.of(SessionPersistPolicy.DEFAULT);
|
|
||||||
|
|
||||||
protected ThreadLocal<SessionSerializationMetadata> currentSessionSerializationMetadata = new ThreadLocal<>();
|
|
||||||
|
|
||||||
enum SessionPersistPolicy {
|
|
||||||
DEFAULT, SAVE_ON_CHANGE, ALWAYS_SAVE_AFTER_REQUEST;
|
|
||||||
|
|
||||||
static SessionPersistPolicy fromName(String name) {
|
|
||||||
for (SessionPersistPolicy policy : SessionPersistPolicy.values()) {
|
|
||||||
if (policy.name().equalsIgnoreCase(name)) {
|
|
||||||
return policy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("Invalid session persist policy [" + name + "]. Must be one of " + Arrays.asList(SessionPersistPolicy.values()) + ".");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSessionPersistPolicies() {
|
|
||||||
StringBuilder policies = new StringBuilder();
|
|
||||||
for (Iterator<SessionPersistPolicy> iter = this.sessionPersistPoliciesSet.iterator(); iter.hasNext();) {
|
|
||||||
SessionPersistPolicy policy = iter.next();
|
|
||||||
policies.append(policy.name());
|
|
||||||
if (iter.hasNext()) {
|
|
||||||
policies.append(",");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return policies.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSessionPersistPolicies(String sessionPersistPolicies) {
|
|
||||||
String[] policyArray = sessionPersistPolicies.split(",");
|
|
||||||
EnumSet<SessionPersistPolicy> policySet = EnumSet.of(SessionPersistPolicy.DEFAULT);
|
|
||||||
for (String policyName : policyArray) {
|
|
||||||
SessionPersistPolicy policy = SessionPersistPolicy.fromName(policyName);
|
|
||||||
policySet.add(policy);
|
|
||||||
}
|
|
||||||
this.sessionPersistPoliciesSet = policySet;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getSaveOnChange() {
|
|
||||||
return this.sessionPersistPoliciesSet.contains(SessionPersistPolicy.SAVE_ON_CHANGE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getAlwaysSaveAfterRequest() {
|
|
||||||
return this.sessionPersistPoliciesSet.contains(SessionPersistPolicy.ALWAYS_SAVE_AFTER_REQUEST);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addLifecycleListener(LifecycleListener listener) {
|
|
||||||
lifecycle.addLifecycleListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public LifecycleListener[] findLifecycleListeners() {
|
|
||||||
return lifecycle.findLifecycleListeners();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeLifecycleListener(LifecycleListener listener) {
|
|
||||||
lifecycle.removeLifecycleListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected synchronized void startInternal() throws LifecycleException {
|
|
||||||
super.startInternal();
|
|
||||||
setState(LifecycleState.STARTING);
|
|
||||||
Boolean attachedToValve = false;
|
|
||||||
for (Valve valve : getContainer().getPipeline().getValves()) {
|
|
||||||
if (valve instanceof RequestSessionHandlerValve) {
|
|
||||||
this.handlerValve = (RequestSessionHandlerValve) valve;
|
|
||||||
this.handlerValve.setRedisSessionManager(this);
|
|
||||||
attachedToValve = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!attachedToValve) {
|
|
||||||
throw new LifecycleException("Unable to attach to session handling valve; sessions cannot be saved after the request without the valve starting properly.");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
initializeSessionSerializer();
|
|
||||||
requestSessionCacheUtils = RequestSessionCacheFactory.getInstance();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error while initializing serializer/rediscache", e);
|
|
||||||
}
|
|
||||||
log.info("The sessions will expire after " + (getContext().getSessionTimeout() * 60) + " seconds");
|
|
||||||
getContext().setDistributable(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected synchronized void stopInternal() throws LifecycleException {
|
|
||||||
setState(LifecycleState.STOPPING);
|
|
||||||
super.stopInternal();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Session createSession(String requestedSessionId) {
|
|
||||||
CustomRequestSession customSession = null;
|
|
||||||
String sessionId = null;
|
|
||||||
if (requestedSessionId != null) {
|
|
||||||
sessionId = requestedSessionId;
|
|
||||||
if (requestSessionCacheUtils.setStringIfKeyNotExists(sessionId.getBytes(), NULL_SESSION) == 0L) {
|
|
||||||
sessionId = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
do {
|
|
||||||
sessionId = generateSessionId();
|
|
||||||
} while (requestSessionCacheUtils.setStringIfKeyNotExists(sessionId.getBytes(), NULL_SESSION) == 0L); // 1 = key set; 0 = key already existed
|
|
||||||
}
|
|
||||||
if (sessionId != null) {
|
|
||||||
customSession = (CustomRequestSession) createEmptySession();
|
|
||||||
customSession.setNew(true);
|
|
||||||
customSession.setValid(true);
|
|
||||||
customSession.setCreationTime(System.currentTimeMillis());
|
|
||||||
customSession.setMaxInactiveInterval((getContext().getSessionTimeout() * 60));
|
|
||||||
customSession.setId(sessionId);
|
|
||||||
customSession.tellNew();
|
|
||||||
}
|
|
||||||
currentSession.set(customSession);
|
|
||||||
currentSessionId.set(sessionId);
|
|
||||||
currentSessionIsPersisted.set(false);
|
|
||||||
currentSessionSerializationMetadata.set(new SessionSerializationMetadata());
|
|
||||||
if (customSession != null) {
|
|
||||||
try {
|
|
||||||
save(customSession, true);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error saving newly created session", e);
|
|
||||||
currentSession.set(null);
|
|
||||||
currentSessionId.set(null);
|
|
||||||
customSession = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return customSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Session createEmptySession() {
|
|
||||||
return new CustomRequestSession(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void add(Session session) {
|
|
||||||
save(session, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Session findSession(String sessionId) throws IOException {
|
|
||||||
CustomRequestSession customSession = null;
|
|
||||||
if (sessionId == null) {
|
|
||||||
currentSessionIsPersisted.set(false);
|
|
||||||
currentSession.set(null);
|
|
||||||
currentSessionSerializationMetadata.set(null);
|
|
||||||
currentSessionId.set(null);
|
|
||||||
} else if (sessionId.equals(currentSessionId.get())) {
|
|
||||||
customSession = currentSession.get();
|
|
||||||
} else {
|
|
||||||
byte[] data = requestSessionCacheUtils.getByteArray(sessionId);
|
|
||||||
if (data != null) {
|
|
||||||
DeserializedSessionContainer container = deserializeSessionData(sessionId, data);
|
|
||||||
customSession = (CustomRequestSession) container.session;
|
|
||||||
currentSession.set(customSession);
|
|
||||||
currentSessionSerializationMetadata.set(container.metadata);
|
|
||||||
currentSessionIsPersisted.set(true);
|
|
||||||
currentSessionId.set(sessionId);
|
|
||||||
} else {
|
|
||||||
currentSessionIsPersisted.set(false);
|
|
||||||
currentSession.set(null);
|
|
||||||
currentSessionSerializationMetadata.set(null);
|
|
||||||
currentSessionId.set(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return customSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void remove(Session session) {
|
|
||||||
remove(session, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void remove(Session session, boolean update) {
|
|
||||||
requestSessionCacheUtils.expire(session.getId(), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void load() throws ClassNotFoundException, IOException {
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unload() throws IOException {
|
|
||||||
// TODO Auto-generated method stub
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* method to deserialize session data
|
|
||||||
*
|
|
||||||
* @param id
|
|
||||||
* @param data
|
|
||||||
* @return
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public DeserializedSessionContainer deserializeSessionData(String id, byte[] data) throws IOException {
|
|
||||||
if (Arrays.equals(NULL_SESSION, data)) {
|
|
||||||
throw new IOException("Serialized session data was equal to NULL_SESSION");
|
|
||||||
}
|
|
||||||
CustomRequestSession customSession = null;
|
|
||||||
SessionSerializationMetadata metadata = null;
|
|
||||||
try {
|
|
||||||
metadata = new SessionSerializationMetadata();
|
|
||||||
customSession = (CustomRequestSession) createEmptySession();
|
|
||||||
serializer.deserializeSessionData(data, customSession, metadata);
|
|
||||||
customSession.setId(id);
|
|
||||||
customSession.setNew(false);
|
|
||||||
customSession.setMaxInactiveInterval((getContext().getSessionTimeout() * 60));
|
|
||||||
customSession.access();
|
|
||||||
customSession.setValid(true);
|
|
||||||
customSession.resetDirtyTracking();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Unable to deserialize into session", e);
|
|
||||||
}
|
|
||||||
return new DeserializedSessionContainer(customSession, metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* method to save session data to cache
|
|
||||||
*
|
|
||||||
* @param session
|
|
||||||
* @param forceSave
|
|
||||||
*/
|
|
||||||
public void save(Session session, boolean forceSave) {
|
|
||||||
Boolean isCurrentSessionPersisted;
|
|
||||||
try {
|
|
||||||
CustomRequestSession customSession = (CustomRequestSession) session;
|
|
||||||
SessionSerializationMetadata sessionSerializationMetadata = currentSessionSerializationMetadata.get();
|
|
||||||
byte[] originalSessionAttributesHash = sessionSerializationMetadata.getSessionAttributesHash();
|
|
||||||
byte[] sessionAttributesHash = null;
|
|
||||||
if (forceSave || customSession.isDirty() || (isCurrentSessionPersisted = this.currentSessionIsPersisted.get()) == null || !isCurrentSessionPersisted || !Arrays.equals(originalSessionAttributesHash, (sessionAttributesHash = serializer.getSessionAttributesHashCode(customSession)))) {
|
|
||||||
if (sessionAttributesHash == null) {
|
|
||||||
sessionAttributesHash = serializer.getSessionAttributesHashCode(customSession);
|
|
||||||
}
|
|
||||||
SessionSerializationMetadata updatedSerializationMetadata = new SessionSerializationMetadata();
|
|
||||||
updatedSerializationMetadata.setSessionAttributesHash(sessionAttributesHash);
|
|
||||||
requestSessionCacheUtils.setByteArray(customSession.getId(), serializer.serializeSessionData(customSession, updatedSerializationMetadata));
|
|
||||||
customSession.resetDirtyTracking();
|
|
||||||
currentSessionSerializationMetadata.set(updatedSerializationMetadata);
|
|
||||||
currentSessionIsPersisted.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
int timeout = getContext().getSessionTimeout() * 60;
|
|
||||||
timeout = timeout < 1800 ? 1800 : timeout;
|
|
||||||
log.trace("Setting expire timeout on session [" + customSession.getId() + "] to " + timeout);
|
|
||||||
requestSessionCacheUtils.expire(customSession.getId(), timeout);
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("Error occured while storing the session object into redis", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void afterRequest(Request request) {
|
|
||||||
CustomRequestSession customSession = currentSession.get();
|
|
||||||
if (customSession != null) {
|
|
||||||
try {
|
|
||||||
if (customSession.isValid()) {
|
|
||||||
log.trace("Request with session completed, saving session " + customSession.getId());
|
|
||||||
save(customSession, getAlwaysSaveAfterRequest());
|
|
||||||
} else {
|
|
||||||
log.trace("HTTP Session has been invalidated, removing :" + customSession.getId());
|
|
||||||
remove(customSession);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Error storing/updating/removing session", e);
|
|
||||||
} finally {
|
|
||||||
currentSession.remove();
|
|
||||||
currentSessionId.remove();
|
|
||||||
currentSessionIsPersisted.remove();
|
|
||||||
log.trace("Session removed from ThreadLocal :" + customSession.getIdInternal());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* method to initialize custom session serializer
|
|
||||||
*
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
private void initializeSessionSerializer() throws Exception {
|
|
||||||
serializer = new SessionDataSerializer();
|
|
||||||
Loader loader = null;
|
|
||||||
if (getContext() != null) {
|
|
||||||
loader = getContext().getLoader();
|
|
||||||
}
|
|
||||||
ClassLoader classLoader = null;
|
|
||||||
if (loader != null) {
|
|
||||||
classLoader = loader.getClassLoader();
|
|
||||||
}
|
|
||||||
serializer.setClassLoader(classLoader);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
package com.r.tomcat.session.management;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.ObjectInputStream;
|
|
||||||
import java.io.ObjectOutputStream;
|
|
||||||
import java.io.Serializable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tomcat clustering implementation
|
|
||||||
*
|
|
||||||
* This class is uses to store and retrieve the HTTP request session objects from catalina to data cache
|
|
||||||
*
|
|
||||||
* @author Ranjith Manickam
|
|
||||||
* @since 1.0
|
|
||||||
*/
|
|
||||||
public class SessionSerializationMetadata implements Serializable
|
|
||||||
{
|
|
||||||
private static final long serialVersionUID = 124438185184833546L;
|
|
||||||
|
|
||||||
private byte[] sessionAttributesHash;
|
|
||||||
|
|
||||||
public SessionSerializationMetadata() {
|
|
||||||
this.sessionAttributesHash = new byte[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] getSessionAttributesHash() {
|
|
||||||
return sessionAttributesHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSessionAttributesHash(byte[] sessionAttributesHash) {
|
|
||||||
this.sessionAttributesHash = sessionAttributesHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void copyFieldsFrom(SessionSerializationMetadata metadata) {
|
|
||||||
this.setSessionAttributesHash(metadata.getSessionAttributesHash());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeObject(ObjectOutputStream out) throws IOException {
|
|
||||||
out.writeInt(sessionAttributesHash.length);
|
|
||||||
out.write(this.sessionAttributesHash);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
|
|
||||||
int hashLength = in.readInt();
|
|
||||||
byte[] sessionAttributesHash = new byte[hashLength];
|
|
||||||
in.read(sessionAttributesHash, 0, hashLength);
|
|
||||||
this.sessionAttributesHash = sessionAttributesHash;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
package com.r.tomcat.session.management;
|
package tomcat.request.session;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
|
@ -10,68 +10,79 @@ import java.io.ObjectOutputStream;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.apache.catalina.util.CustomObjectInputStream;
|
import org.apache.catalina.util.CustomObjectInputStream;
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tomcat clustering implementation
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
*
|
*
|
||||||
* This class is uses to store and retrieve the HTTP request session objects from catalina to data cache
|
* Session serialization utility.
|
||||||
*
|
*
|
||||||
* @author Ranjith Manickam
|
* @author Ranjith Manickam
|
||||||
* @since 1.0
|
* @since 2.0
|
||||||
*/
|
*/
|
||||||
public class SessionDataSerializer
|
public class SerializationUtil {
|
||||||
{
|
|
||||||
private final Log log = LogFactory.getLog(SessionDataSerializer.class);
|
|
||||||
|
|
||||||
private ClassLoader loader;
|
private ClassLoader loader;
|
||||||
|
|
||||||
|
private Log log = LogFactory.getLog(SerializationUtil.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To set class loader
|
||||||
|
*
|
||||||
|
* @param loader
|
||||||
|
*/
|
||||||
public void setClassLoader(ClassLoader loader) {
|
public void setClassLoader(ClassLoader loader) {
|
||||||
this.loader = loader;
|
this.loader = loader;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* method to get session attributes hash code
|
* To get session attributes hash code
|
||||||
*
|
*
|
||||||
* @param session
|
* @param session
|
||||||
* @return
|
* @return
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
public byte[] getSessionAttributesHashCode(CustomRequestSession session) throws IOException {
|
public byte[] getSessionAttributesHashCode(Session session) throws IOException {
|
||||||
byte[] serialized = null;
|
byte[] serialized = null;
|
||||||
HashMap<String, Object> attributes = new HashMap<String, Object>();
|
Map<String, Object> attributes = new HashMap<String, Object>();
|
||||||
|
|
||||||
for (Enumeration<String> enumerator = session.getAttributeNames(); enumerator.hasMoreElements();) {
|
for (Enumeration<String> enumerator = session.getAttributeNames(); enumerator.hasMoreElements();) {
|
||||||
String key = enumerator.nextElement();
|
String key = enumerator.nextElement();
|
||||||
attributes.put(key, session.getAttribute(key));
|
attributes.put(key, session.getAttribute(key));
|
||||||
}
|
}
|
||||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos));) {
|
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos));) {
|
||||||
oos.writeUnshared(attributes);
|
oos.writeUnshared(attributes);
|
||||||
oos.flush();
|
oos.flush();
|
||||||
serialized = bos.toByteArray();
|
serialized = bos.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDigest digester = null;
|
MessageDigest digester = null;
|
||||||
try {
|
try {
|
||||||
digester = MessageDigest.getInstance("MD5");
|
digester = MessageDigest.getInstance("MD5");
|
||||||
} catch (Exception e) {
|
} catch (Exception ex) {
|
||||||
log.error("Unable to get MessageDigest instance for MD5");
|
log.error("Unable to get MessageDigest instance for MD5", ex);
|
||||||
}
|
}
|
||||||
return digester.digest(serialized);
|
return digester.digest(serialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* method to serialize custom session data
|
* To serialize session object
|
||||||
*
|
*
|
||||||
* @param session
|
* @param session
|
||||||
* @param metadata
|
* @param metadata
|
||||||
* @return
|
* @return
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
public byte[] serializeSessionData(CustomRequestSession session, SessionSerializationMetadata metadata) throws IOException {
|
public byte[] serializeSessionData(Session session, SessionMetadata metadata) throws IOException {
|
||||||
byte[] serialized = null;
|
byte[] serialized = null;
|
||||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos));) {
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos));) {
|
||||||
oos.writeObject(metadata);
|
oos.writeObject(metadata);
|
||||||
session.writeObjectData(oos);
|
session.writeObjectData(oos);
|
||||||
oos.flush();
|
oos.flush();
|
||||||
|
@ -81,7 +92,7 @@ public class SessionDataSerializer
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* method to deserialize custom session data
|
* To de-serialize session object
|
||||||
*
|
*
|
||||||
* @param data
|
* @param data
|
||||||
* @param session
|
* @param session
|
||||||
|
@ -89,9 +100,11 @@ public class SessionDataSerializer
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
* @throws ClassNotFoundException
|
* @throws ClassNotFoundException
|
||||||
*/
|
*/
|
||||||
public void deserializeSessionData(byte[] data, CustomRequestSession session, SessionSerializationMetadata metadata) throws IOException, ClassNotFoundException {
|
public void deserializeSessionData(byte[] data, Session session, SessionMetadata metadata)
|
||||||
try (BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data)); ObjectInputStream ois = new CustomObjectInputStream(bis, loader);) {
|
throws IOException, ClassNotFoundException {
|
||||||
SessionSerializationMetadata serializedMetadata = (SessionSerializationMetadata) ois.readObject();
|
try (BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data));
|
||||||
|
ObjectInputStream ois = new CustomObjectInputStream(bis, this.loader);) {
|
||||||
|
SessionMetadata serializedMetadata = (SessionMetadata) ois.readObject();
|
||||||
metadata.copyFieldsFrom(serializedMetadata);
|
metadata.copyFieldsFrom(serializedMetadata);
|
||||||
session.readObjectData(ois);
|
session.readObjectData(ois);
|
||||||
}
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package tomcat.request.session;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.security.Principal;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.apache.catalina.Manager;
|
||||||
|
import org.apache.catalina.session.StandardSession;
|
||||||
|
|
||||||
|
import tomcat.request.session.redis.SessionManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
|
*
|
||||||
|
* This class is uses to store and retrieve the HTTP request session objects.
|
||||||
|
*
|
||||||
|
* @author Ranjith Manickam
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
public class Session extends StandardSession {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = -6056744304016869278L;
|
||||||
|
|
||||||
|
protected Boolean dirty;
|
||||||
|
|
||||||
|
protected Map<String, Object> changedAttributes;
|
||||||
|
|
||||||
|
protected static Boolean manualDirtyTrackingSupportEnabled = false;
|
||||||
|
|
||||||
|
protected static String manualDirtyTrackingAttributeKey = "__changed__";
|
||||||
|
|
||||||
|
public Session(Manager manager) {
|
||||||
|
super(manager);
|
||||||
|
resetDirtyTracking();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetDirtyTracking() {
|
||||||
|
this.changedAttributes = new HashMap<>();
|
||||||
|
this.dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setManualDirtyTrackingSupportEnabled(boolean enabled) {
|
||||||
|
manualDirtyTrackingSupportEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setManualDirtyTrackingAttributeKey(String key) {
|
||||||
|
manualDirtyTrackingAttributeKey = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean isDirty() {
|
||||||
|
return this.dirty || !this.changedAttributes.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getChangedAttributes() {
|
||||||
|
return this.changedAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void setAttribute(String key, Object value) {
|
||||||
|
if (manualDirtyTrackingSupportEnabled && manualDirtyTrackingAttributeKey.equals(key)) {
|
||||||
|
this.dirty = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object oldValue = getAttribute(key);
|
||||||
|
super.setAttribute(key, value);
|
||||||
|
|
||||||
|
if ((value != null || oldValue != null)
|
||||||
|
&& (value == null && oldValue != null || oldValue == null && value != null
|
||||||
|
|| !value.getClass().isInstance(oldValue) || !value.equals(oldValue))) {
|
||||||
|
if (this.manager instanceof SessionManager && ((SessionManager) this.manager).getSaveOnChange()) {
|
||||||
|
((SessionManager) this.manager).save(this, true);
|
||||||
|
} else {
|
||||||
|
this.changedAttributes.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Object getAttribute(String name) {
|
||||||
|
return super.getAttribute(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Enumeration<String> getAttributeNames() {
|
||||||
|
return super.getAttributeNames();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void removeAttribute(String name) {
|
||||||
|
super.removeAttribute(name);
|
||||||
|
if (this.manager instanceof SessionManager && ((SessionManager) this.manager).getSaveOnChange()) {
|
||||||
|
((SessionManager) this.manager).save(this, true);
|
||||||
|
} else {
|
||||||
|
this.dirty = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void setPrincipal(Principal principal) {
|
||||||
|
super.setPrincipal(principal);
|
||||||
|
this.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void writeObjectData(ObjectOutputStream out) throws IOException {
|
||||||
|
super.writeObjectData(out);
|
||||||
|
out.writeLong(this.getCreationTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void readObjectData(ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||||
|
super.readObjectData(in);
|
||||||
|
this.setCreationTime(in.readLong());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package tomcat.request.session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
|
*
|
||||||
|
* Session constants.
|
||||||
|
*
|
||||||
|
* @author Ranjith Manickam
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
public class SessionConstants {
|
||||||
|
|
||||||
|
public static final byte[] NULL_SESSION = "null".getBytes();
|
||||||
|
|
||||||
|
public static final String CATALINA_BASE = "catalina.base";
|
||||||
|
|
||||||
|
public static final String CONF = "conf";
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package tomcat.request.session;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
|
*
|
||||||
|
* Session context uses to manage current session data.
|
||||||
|
*
|
||||||
|
* @author Ranjith Manickam
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
public class SessionContext {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private Session session;
|
||||||
|
|
||||||
|
private boolean persisted;
|
||||||
|
|
||||||
|
private SessionMetadata metadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To get session id
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To set session id
|
||||||
|
*
|
||||||
|
* @param id
|
||||||
|
*/
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To get session
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public Session getSession() {
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To set session
|
||||||
|
*
|
||||||
|
* @param session
|
||||||
|
*/
|
||||||
|
public void setSession(Session session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To check session is persisted
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean isPersisted() {
|
||||||
|
return persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To set session persisted
|
||||||
|
*
|
||||||
|
* @param persisted
|
||||||
|
*/
|
||||||
|
public void setPersisted(boolean persisted) {
|
||||||
|
this.persisted = persisted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To get session meta-data
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public SessionMetadata getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To set session meta-data
|
||||||
|
*
|
||||||
|
* @param metadata
|
||||||
|
*/
|
||||||
|
public void setMetadata(SessionMetadata metadata) {
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SessionContext [id=" + id + "]";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package tomcat.request.session;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
|
*
|
||||||
|
* This class is uses to store and retrieve the HTTP request session object
|
||||||
|
* meta-data.
|
||||||
|
*
|
||||||
|
* @author Ranjith Manickam
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
public class SessionMetadata implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 124438185184833546L;
|
||||||
|
|
||||||
|
private byte[] attributesHash;
|
||||||
|
|
||||||
|
public SessionMetadata() {
|
||||||
|
this.attributesHash = new byte[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To get session meta-data hash
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public byte[] getAttributesHash() {
|
||||||
|
return this.attributesHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To set session meta-data hash
|
||||||
|
*
|
||||||
|
* @param attributesHash
|
||||||
|
*/
|
||||||
|
public void setAttributesHash(byte[] attributesHash) {
|
||||||
|
this.attributesHash = attributesHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To copy session meta-data
|
||||||
|
*
|
||||||
|
* @param metadata
|
||||||
|
*/
|
||||||
|
public void copyFieldsFrom(SessionMetadata metadata) {
|
||||||
|
this.setAttributesHash(metadata.getAttributesHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To write session meta-data to output stream
|
||||||
|
*
|
||||||
|
* @param out
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private void writeObject(ObjectOutputStream out) throws IOException {
|
||||||
|
out.writeInt(this.attributesHash.length);
|
||||||
|
out.write(this.attributesHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To read session meta-data from input stream
|
||||||
|
*
|
||||||
|
* @param in
|
||||||
|
* @throws IOException
|
||||||
|
* @throws ClassNotFoundException
|
||||||
|
*/
|
||||||
|
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||||
|
int hashLength = in.readInt();
|
||||||
|
byte[] attributesHash = new byte[hashLength];
|
||||||
|
in.read(attributesHash, 0, hashLength);
|
||||||
|
this.attributesHash = attributesHash;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package tomcat.request.session.data.cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
|
*
|
||||||
|
* API for Data cache.
|
||||||
|
*
|
||||||
|
* @author Ranjith Manickam
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
public interface DataCache {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To set value in data-cache
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
byte[] set(String key, byte[] value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To set value if key not exists in data-cache
|
||||||
|
*
|
||||||
|
* Returns If key exists = 0 else 1
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @param value
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Long setnx(String key, byte[] value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To expire the value based on key in data-cache
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @param seconds
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Long expire(String key, int seconds);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To get the value based on key from data-cache
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
byte[] get(String key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To delete the value based on key from data-cache
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
Long delete(String key);
|
||||||
|
|
||||||
|
}
|
|
@ -1,18 +1,22 @@
|
||||||
package com.r.tomcat.session.data.cache.constants;
|
package tomcat.request.session.data.cache.impl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tomcat clustering implementation
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
|
*
|
||||||
|
* Redis data-cache constants.
|
||||||
*
|
*
|
||||||
* Redis data cache constants
|
|
||||||
*
|
|
||||||
* @author Ranjith Manickam
|
* @author Ranjith Manickam
|
||||||
* @since 1.0
|
* @since 2.0
|
||||||
*/
|
*/
|
||||||
public class RedisConstants
|
public class RedisConstants {
|
||||||
{
|
|
||||||
public static final String REDIS_DATA_CACHE_PROPERTIES_FILE = "RedisDataCache.properties";
|
// redis properties file name
|
||||||
|
public static final String PROPERTIES_FILE = "redis-data-cache.properties";
|
||||||
// Redis properties
|
|
||||||
|
// redis properties
|
||||||
|
public static final String HOSTS = "redis.hosts";
|
||||||
|
public static final String CLUSTER_ENABLED = "redis.cluster.enabled";
|
||||||
|
|
||||||
public static final String MAX_ACTIVE = "redis.max.active";
|
public static final String MAX_ACTIVE = "redis.max.active";
|
||||||
public static final String TEST_ONBORROW = "redis.test.onBorrow";
|
public static final String TEST_ONBORROW = "redis.test.onBorrow";
|
||||||
public static final String TEST_ONRETURN = "redis.test.onReturn";
|
public static final String TEST_ONRETURN = "redis.test.onReturn";
|
||||||
|
@ -21,13 +25,12 @@ public class RedisConstants
|
||||||
public static final String TEST_WHILEIDLE = "redis.test.whileIdle";
|
public static final String TEST_WHILEIDLE = "redis.test.whileIdle";
|
||||||
public static final String TEST_NUMPEREVICTION = "redis.test.numPerEviction";
|
public static final String TEST_NUMPEREVICTION = "redis.test.numPerEviction";
|
||||||
public static final String TIME_BETWEENEVICTION = "redis.time.betweenEviction";
|
public static final String TIME_BETWEENEVICTION = "redis.time.betweenEviction";
|
||||||
public static final String HOSTS = "redis.hosts";
|
|
||||||
public static final String PASSWORD = "redis.password";
|
public static final String PASSWORD = "redis.password";
|
||||||
public static final String IS_CLUSTER_ENABLED = "redis.cluster.enabled";
|
|
||||||
public static final String DATABASE = "redis.database";
|
public static final String DATABASE = "redis.database";
|
||||||
public static final String TIMEOUT = "redis.timeout";
|
public static final String TIMEOUT = "redis.timeout";
|
||||||
|
|
||||||
// Redis property default values
|
// redis property default values
|
||||||
public static final String DEFAULT_MAX_ACTIVE_VALUE = "10";
|
public static final String DEFAULT_MAX_ACTIVE_VALUE = "10";
|
||||||
public static final String DEFAULT_TEST_ONBORROW_VALUE = "true";
|
public static final String DEFAULT_TEST_ONBORROW_VALUE = "true";
|
||||||
public static final String DEFAULT_TEST_ONRETURN_VALUE = "true";
|
public static final String DEFAULT_TEST_ONRETURN_VALUE = "true";
|
||||||
|
@ -36,5 +39,7 @@ public class RedisConstants
|
||||||
public static final String DEFAULT_TEST_WHILEIDLE_VALUE = "true";
|
public static final String DEFAULT_TEST_WHILEIDLE_VALUE = "true";
|
||||||
public static final String DEFAULT_TEST_NUMPEREVICTION_VALUE = "10";
|
public static final String DEFAULT_TEST_NUMPEREVICTION_VALUE = "10";
|
||||||
public static final String DEFAULT_TIME_BETWEENEVICTION_VALUE = "60000";
|
public static final String DEFAULT_TIME_BETWEENEVICTION_VALUE = "60000";
|
||||||
public static final String DEFAULT_IS_CLUSTER_ENABLED = "false";
|
public static final String DEFAULT_CLUSTER_ENABLED = "false";
|
||||||
|
|
||||||
|
public static final String CONN_FAILED_RETRY_MSG = "Jedis connection failed, retrying...";
|
||||||
}
|
}
|
|
@ -0,0 +1,492 @@
|
||||||
|
package tomcat.request.session.data.cache.impl;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import redis.clients.jedis.HostAndPort;
|
||||||
|
import redis.clients.jedis.Jedis;
|
||||||
|
import redis.clients.jedis.JedisCluster;
|
||||||
|
import redis.clients.jedis.JedisPool;
|
||||||
|
import redis.clients.jedis.JedisPoolConfig;
|
||||||
|
import redis.clients.jedis.Protocol;
|
||||||
|
import redis.clients.jedis.exceptions.JedisClusterMaxRedirectionsException;
|
||||||
|
import redis.clients.jedis.exceptions.JedisConnectionException;
|
||||||
|
import tomcat.request.session.SessionConstants;
|
||||||
|
import tomcat.request.session.data.cache.DataCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
|
*
|
||||||
|
* Redis data-cache implementation to store/retrieve session objects.
|
||||||
|
*
|
||||||
|
* @author Ranjith Manickam
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
public class RedisDataCache implements DataCache {
|
||||||
|
|
||||||
|
private static DataCache dataCache;
|
||||||
|
|
||||||
|
private Log log = LogFactory.getLog(RedisDataCache.class);
|
||||||
|
|
||||||
|
public RedisDataCache() {
|
||||||
|
initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public byte[] set(String key, byte[] value) {
|
||||||
|
return dataCache.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Long setnx(String key, byte[] value) {
|
||||||
|
return dataCache.setnx(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Long expire(String key, int seconds) {
|
||||||
|
return dataCache.expire(key, seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public byte[] get(String key) {
|
||||||
|
return (key != null) ? dataCache.get(key) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Long delete(String key) {
|
||||||
|
return dataCache.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To parse data-cache key
|
||||||
|
*
|
||||||
|
* @param key
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static String parseDataCacheKey(String key) {
|
||||||
|
return key.replaceAll("\\s", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To initialize the data-cache
|
||||||
|
*
|
||||||
|
* @param properties
|
||||||
|
* @param filePath
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void initialize() {
|
||||||
|
if (dataCache != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Properties properties = loadProperties();
|
||||||
|
|
||||||
|
boolean clusterEnabled = Boolean.valueOf(properties.getProperty(RedisConstants.CLUSTER_ENABLED, RedisConstants.DEFAULT_CLUSTER_ENABLED));
|
||||||
|
|
||||||
|
String hosts = properties.getProperty(RedisConstants.HOSTS, Protocol.DEFAULT_HOST.concat(":").concat(String.valueOf(Protocol.DEFAULT_PORT)));
|
||||||
|
Collection<? extends Serializable> nodes = getJedisNodes(hosts, clusterEnabled);
|
||||||
|
|
||||||
|
String password = properties.getProperty(RedisConstants.PASSWORD);
|
||||||
|
password = (password != null && !password.isEmpty()) ? password : null;
|
||||||
|
|
||||||
|
int database = Integer.parseInt(properties.getProperty(RedisConstants.DATABASE, String.valueOf(Protocol.DEFAULT_DATABASE)));
|
||||||
|
|
||||||
|
int timeout = Integer.parseInt(properties.getProperty(RedisConstants.TIMEOUT, String.valueOf(Protocol.DEFAULT_TIMEOUT)));
|
||||||
|
timeout = (timeout < Protocol.DEFAULT_TIMEOUT) ? Protocol.DEFAULT_TIMEOUT : timeout;
|
||||||
|
|
||||||
|
if (clusterEnabled) {
|
||||||
|
dataCache = new RedisClusterCacheUtil((Set<HostAndPort>) nodes, timeout, getPoolConfig(properties));
|
||||||
|
} else {
|
||||||
|
dataCache = new RedisCacheUtil(((List<String>) nodes).get(0),
|
||||||
|
Integer.parseInt(((List<String>) nodes).get(1)), password, database, timeout, getPoolConfig(properties));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To get jedis pool configuration
|
||||||
|
*
|
||||||
|
* @param properties
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private JedisPoolConfig getPoolConfig(Properties properties) {
|
||||||
|
JedisPoolConfig poolConfig = new JedisPoolConfig();
|
||||||
|
int maxActive = Integer.parseInt(properties.getProperty(RedisConstants.MAX_ACTIVE, RedisConstants.DEFAULT_MAX_ACTIVE_VALUE));
|
||||||
|
poolConfig.setMaxTotal(maxActive);
|
||||||
|
|
||||||
|
boolean testOnBorrow = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_ONBORROW, RedisConstants.DEFAULT_TEST_ONBORROW_VALUE));
|
||||||
|
poolConfig.setTestOnBorrow(testOnBorrow);
|
||||||
|
|
||||||
|
boolean testOnReturn = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_ONRETURN, RedisConstants.DEFAULT_TEST_ONRETURN_VALUE));
|
||||||
|
poolConfig.setTestOnReturn(testOnReturn);
|
||||||
|
|
||||||
|
int maxIdle = Integer.parseInt(properties.getProperty(RedisConstants.MAX_ACTIVE, RedisConstants.DEFAULT_MAX_ACTIVE_VALUE));
|
||||||
|
poolConfig.setMaxIdle(maxIdle);
|
||||||
|
|
||||||
|
int minIdle = Integer.parseInt(properties.getProperty(RedisConstants.MIN_IDLE, RedisConstants.DEFAULT_MIN_IDLE_VALUE));
|
||||||
|
poolConfig.setMinIdle(minIdle);
|
||||||
|
|
||||||
|
boolean testWhileIdle = Boolean.parseBoolean(properties.getProperty(RedisConstants.TEST_WHILEIDLE, RedisConstants.DEFAULT_TEST_WHILEIDLE_VALUE));
|
||||||
|
poolConfig.setTestWhileIdle(testWhileIdle);
|
||||||
|
|
||||||
|
int testNumPerEviction = Integer.parseInt(properties.getProperty(RedisConstants.TEST_NUMPEREVICTION, RedisConstants.DEFAULT_TEST_NUMPEREVICTION_VALUE));
|
||||||
|
poolConfig.setNumTestsPerEvictionRun(testNumPerEviction);
|
||||||
|
|
||||||
|
long timeBetweenEviction = Long.parseLong(properties.getProperty(RedisConstants.TIME_BETWEENEVICTION, RedisConstants.DEFAULT_TIME_BETWEENEVICTION_VALUE));
|
||||||
|
poolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEviction);
|
||||||
|
return poolConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To get jedis nodes
|
||||||
|
*
|
||||||
|
* @param hosts
|
||||||
|
* @param clusterEnabled
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private Collection<? extends Serializable> getJedisNodes(String hosts, boolean clusterEnabled) {
|
||||||
|
hosts = hosts.replaceAll("\\s", "");
|
||||||
|
String[] hostPorts = hosts.split(",");
|
||||||
|
|
||||||
|
List<String> node = null;
|
||||||
|
Set<HostAndPort> nodes = null;
|
||||||
|
|
||||||
|
for (String hostPort : hostPorts) {
|
||||||
|
String[] hostPortArr = hostPort.split(":");
|
||||||
|
|
||||||
|
if (clusterEnabled) {
|
||||||
|
nodes = (nodes == null) ? new HashSet<HostAndPort>() : nodes;
|
||||||
|
nodes.add(new HostAndPort(hostPortArr[0], Integer.valueOf(hostPortArr[1])));
|
||||||
|
} else {
|
||||||
|
int port = Integer.valueOf(hostPortArr[1]);
|
||||||
|
if (!hostPortArr[0].isEmpty() && port > 0) {
|
||||||
|
node = (node == null) ? new ArrayList<String>() : node;
|
||||||
|
node.add(hostPortArr[0]);
|
||||||
|
node.add(String.valueOf(port));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clusterEnabled ? nodes : node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To load data-cache properties
|
||||||
|
*
|
||||||
|
* @param filePath
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private Properties loadProperties() {
|
||||||
|
Properties properties = new Properties();
|
||||||
|
try {
|
||||||
|
String filePath = System.getProperty(SessionConstants.CATALINA_BASE).concat(File.separator)
|
||||||
|
.concat(SessionConstants.CONF).concat(File.separator).concat(RedisConstants.PROPERTIES_FILE);
|
||||||
|
|
||||||
|
InputStream resourceStream = null;
|
||||||
|
try {
|
||||||
|
resourceStream = (filePath != null && !filePath.isEmpty() && new File(filePath).exists())
|
||||||
|
? new FileInputStream(filePath) : null;
|
||||||
|
|
||||||
|
if (resourceStream == null) {
|
||||||
|
ClassLoader loader = Thread.currentThread().getContextClassLoader();
|
||||||
|
resourceStream = loader.getResourceAsStream(RedisConstants.PROPERTIES_FILE);
|
||||||
|
}
|
||||||
|
properties.load(resourceStream);
|
||||||
|
} finally {
|
||||||
|
resourceStream.close();
|
||||||
|
}
|
||||||
|
} catch (IOException ex) {
|
||||||
|
log.error("Error while loading task scheduler properties", ex);
|
||||||
|
}
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
|
*
|
||||||
|
* Redis stand-alone mode data-cache implementation.
|
||||||
|
*
|
||||||
|
* @author Ranjith Manickam
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
private class RedisCacheUtil implements DataCache {
|
||||||
|
|
||||||
|
private JedisPool pool;
|
||||||
|
|
||||||
|
private final int numRetries = 3;
|
||||||
|
|
||||||
|
private Log log = LogFactory.getLog(RedisCacheUtil.class);
|
||||||
|
|
||||||
|
public RedisCacheUtil(String host, int port, String password, int database, int timeout,
|
||||||
|
JedisPoolConfig poolConfig) {
|
||||||
|
pool = new JedisPool(poolConfig, host, port, timeout, password, database);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public byte[] set(String key, byte[] value) {
|
||||||
|
int tries = 0;
|
||||||
|
boolean sucess = false;
|
||||||
|
String retVal = null;
|
||||||
|
do {
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
Jedis jedis = pool.getResource();
|
||||||
|
retVal = jedis.set(key.getBytes(), value);
|
||||||
|
jedis.close();
|
||||||
|
sucess = true;
|
||||||
|
} catch (JedisConnectionException ex) {
|
||||||
|
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
|
||||||
|
if (tries == numRetries)
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
} while (!sucess && tries <= numRetries);
|
||||||
|
return (retVal != null) ? retVal.getBytes() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Long setnx(String key, byte[] value) {
|
||||||
|
int tries = 0;
|
||||||
|
boolean sucess = false;
|
||||||
|
Long retVal = null;
|
||||||
|
do {
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
Jedis jedis = pool.getResource();
|
||||||
|
retVal = jedis.setnx(key.getBytes(), value);
|
||||||
|
jedis.close();
|
||||||
|
sucess = true;
|
||||||
|
} catch (JedisConnectionException ex) {
|
||||||
|
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
|
||||||
|
if (tries == numRetries)
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
} while (!sucess && tries <= numRetries);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Long expire(String key, int seconds) {
|
||||||
|
int tries = 0;
|
||||||
|
boolean sucess = false;
|
||||||
|
Long retVal = null;
|
||||||
|
do {
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
Jedis jedis = pool.getResource();
|
||||||
|
retVal = jedis.expire(key, seconds);
|
||||||
|
jedis.close();
|
||||||
|
sucess = true;
|
||||||
|
} catch (JedisConnectionException ex) {
|
||||||
|
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
|
||||||
|
if (tries == numRetries)
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
} while (!sucess && tries <= numRetries);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public byte[] get(String key) {
|
||||||
|
int tries = 0;
|
||||||
|
boolean sucess = false;
|
||||||
|
byte[] retVal = null;
|
||||||
|
do {
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
Jedis jedis = pool.getResource();
|
||||||
|
retVal = jedis.get(key.getBytes());
|
||||||
|
jedis.close();
|
||||||
|
sucess = true;
|
||||||
|
} catch (JedisConnectionException ex) {
|
||||||
|
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
|
||||||
|
if (tries == numRetries)
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
} while (!sucess && tries <= numRetries);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Long delete(String key) {
|
||||||
|
int tries = 0;
|
||||||
|
boolean sucess = false;
|
||||||
|
Long retVal = null;
|
||||||
|
do {
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
Jedis jedis = pool.getResource();
|
||||||
|
retVal = jedis.del(key);
|
||||||
|
jedis.close();
|
||||||
|
sucess = true;
|
||||||
|
} catch (JedisConnectionException ex) {
|
||||||
|
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
|
||||||
|
if (tries == numRetries)
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
} while (!sucess && tries <= numRetries);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
|
*
|
||||||
|
* Redis multiple node cluster data-cache implementation.
|
||||||
|
*
|
||||||
|
* @author Ranjith Manickam
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
private class RedisClusterCacheUtil implements DataCache {
|
||||||
|
|
||||||
|
private JedisCluster cluster;
|
||||||
|
|
||||||
|
private final int numRetries = 30;
|
||||||
|
|
||||||
|
private Log log = LogFactory.getLog(RedisClusterCacheUtil.class);
|
||||||
|
|
||||||
|
public RedisClusterCacheUtil(Set<HostAndPort> nodes, int timeout, JedisPoolConfig poolConfig) {
|
||||||
|
cluster = new JedisCluster(nodes, timeout, poolConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public byte[] set(String key, byte[] value) {
|
||||||
|
int tries = 0;
|
||||||
|
boolean sucess = false;
|
||||||
|
String retVal = null;
|
||||||
|
do {
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
retVal = cluster.set(key.getBytes(), value);
|
||||||
|
sucess = true;
|
||||||
|
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) {
|
||||||
|
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
|
||||||
|
if (tries == numRetries) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
waitforFailover();
|
||||||
|
}
|
||||||
|
} while (!sucess && tries <= numRetries);
|
||||||
|
return (retVal != null) ? retVal.getBytes() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Long setnx(String key, byte[] value) {
|
||||||
|
int tries = 0;
|
||||||
|
boolean sucess = false;
|
||||||
|
Long retVal = null;
|
||||||
|
do {
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
retVal = cluster.setnx(key.getBytes(), value);
|
||||||
|
sucess = true;
|
||||||
|
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) {
|
||||||
|
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
|
||||||
|
if (tries == numRetries) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
waitforFailover();
|
||||||
|
}
|
||||||
|
} while (!sucess && tries <= numRetries);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Long expire(String key, int seconds) {
|
||||||
|
int tries = 0;
|
||||||
|
boolean sucess = false;
|
||||||
|
Long retVal = null;
|
||||||
|
do {
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
retVal = cluster.expire(key, seconds);
|
||||||
|
sucess = true;
|
||||||
|
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) {
|
||||||
|
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
|
||||||
|
if (tries == numRetries) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
waitforFailover();
|
||||||
|
}
|
||||||
|
} while (!sucess && tries <= numRetries);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public byte[] get(String key) {
|
||||||
|
int tries = 0;
|
||||||
|
boolean sucess = false;
|
||||||
|
byte[] retVal = null;
|
||||||
|
do {
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
retVal = cluster.get(key.getBytes());
|
||||||
|
sucess = true;
|
||||||
|
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) {
|
||||||
|
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
|
||||||
|
if (tries == numRetries) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
waitforFailover();
|
||||||
|
}
|
||||||
|
} while (!sucess && tries <= numRetries);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Long delete(String key) {
|
||||||
|
int tries = 0;
|
||||||
|
boolean sucess = false;
|
||||||
|
Long retVal = null;
|
||||||
|
do {
|
||||||
|
tries++;
|
||||||
|
try {
|
||||||
|
retVal = cluster.del(key);
|
||||||
|
sucess = true;
|
||||||
|
} catch (JedisClusterMaxRedirectionsException | JedisConnectionException ex) {
|
||||||
|
log.error(RedisConstants.CONN_FAILED_RETRY_MSG + tries);
|
||||||
|
if (tries == numRetries) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
waitforFailover();
|
||||||
|
}
|
||||||
|
} while (!sucess && tries <= numRetries);
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To wait for handling redis fail-over
|
||||||
|
*/
|
||||||
|
private void waitforFailover() {
|
||||||
|
try {
|
||||||
|
Thread.sleep(4000);
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package com.r.tomcat.session.management;
|
package tomcat.request.session.redis;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@ -9,21 +9,28 @@ import org.apache.catalina.connector.Response;
|
||||||
import org.apache.catalina.valves.ValveBase;
|
import org.apache.catalina.valves.ValveBase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tomcat clustering implementation
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
*
|
*
|
||||||
* This class is uses to store and retrieve the HTTP request session objects from catalina to data cache
|
* Valve that implements per-request session persistence. It is intended to be
|
||||||
|
* used with non-sticky load-balancers.
|
||||||
*
|
*
|
||||||
* @author Ranjith Manickam
|
* @author Ranjith Manickam
|
||||||
* @since 1.0
|
* @since 2.0
|
||||||
*/
|
*/
|
||||||
public class RequestSessionHandlerValve extends ValveBase
|
public class SessionHandlerValve extends ValveBase {
|
||||||
{
|
|
||||||
private RequestSessionManager manager;
|
|
||||||
|
|
||||||
public void setRedisSessionManager(RequestSessionManager manager) {
|
private SessionManager manager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To set session manager
|
||||||
|
*
|
||||||
|
* @param manager
|
||||||
|
*/
|
||||||
|
public void setSessionManager(SessionManager manager) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
@Override
|
@Override
|
||||||
public void invoke(Request request, Response response) throws IOException, ServletException {
|
public void invoke(Request request, Response response) throws IOException, ServletException {
|
||||||
try {
|
try {
|
|
@ -0,0 +1,389 @@
|
||||||
|
package tomcat.request.session.redis;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.apache.catalina.Lifecycle;
|
||||||
|
import org.apache.catalina.LifecycleException;
|
||||||
|
import org.apache.catalina.LifecycleListener;
|
||||||
|
import org.apache.catalina.LifecycleState;
|
||||||
|
import org.apache.catalina.Valve;
|
||||||
|
import org.apache.catalina.connector.Request;
|
||||||
|
import org.apache.catalina.session.ManagerBase;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
|
import tomcat.request.session.SerializationUtil;
|
||||||
|
import tomcat.request.session.Session;
|
||||||
|
import tomcat.request.session.SessionConstants;
|
||||||
|
import tomcat.request.session.SessionContext;
|
||||||
|
import tomcat.request.session.SessionMetadata;
|
||||||
|
import tomcat.request.session.data.cache.DataCache;
|
||||||
|
import tomcat.request.session.data.cache.impl.RedisDataCache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
|
*
|
||||||
|
* Manager that implements per-request session persistence. It is intended to be
|
||||||
|
* used with non-sticky load-balancers.
|
||||||
|
*
|
||||||
|
* @author Ranjith Manickam
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
public class SessionManager extends ManagerBase implements Lifecycle {
|
||||||
|
|
||||||
|
private DataCache dataCache;
|
||||||
|
|
||||||
|
protected SerializationUtil serializer;
|
||||||
|
|
||||||
|
protected ThreadLocal<SessionContext> sessionContext = new ThreadLocal<>();
|
||||||
|
|
||||||
|
protected SessionHandlerValve handlerValve;
|
||||||
|
|
||||||
|
protected Set<SessionPolicy> sessionPolicy = EnumSet.of(SessionPolicy.DEFAULT);
|
||||||
|
|
||||||
|
private Log log = LogFactory.getLog(SessionManager.class);
|
||||||
|
|
||||||
|
enum SessionPolicy {
|
||||||
|
DEFAULT, SAVE_ON_CHANGE, ALWAYS_SAVE_AFTER_REQUEST;
|
||||||
|
|
||||||
|
static SessionPolicy fromName(String name) {
|
||||||
|
for (SessionPolicy policy : SessionPolicy.values()) {
|
||||||
|
if (policy.name().equalsIgnoreCase(name)) {
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Invalid session policy [" + name + "]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To get session persist policies
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public String getSessionPersistPolicies() {
|
||||||
|
String policyStr = null;
|
||||||
|
for (SessionPolicy policy : this.sessionPolicy) {
|
||||||
|
policyStr = (policyStr == null) ? policy.name() : policyStr.concat(",").concat(policy.name());
|
||||||
|
}
|
||||||
|
return policyStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To set session persist policies
|
||||||
|
*
|
||||||
|
* @param policyStr
|
||||||
|
*/
|
||||||
|
public void setSessionPersistPolicies(String policyStr) {
|
||||||
|
Set<SessionPolicy> policySet = EnumSet.of(SessionPolicy.DEFAULT);
|
||||||
|
String[] policyArray = policyStr.split(",");
|
||||||
|
|
||||||
|
for (String policy : policyArray) {
|
||||||
|
policySet.add(SessionPolicy.fromName(policy));
|
||||||
|
}
|
||||||
|
this.sessionPolicy = policySet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean getSaveOnChange() {
|
||||||
|
return this.sessionPolicy.contains(SessionPolicy.SAVE_ON_CHANGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean getAlwaysSaveAfterRequest() {
|
||||||
|
return this.sessionPolicy.contains(SessionPolicy.ALWAYS_SAVE_AFTER_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void addLifecycleListener(LifecycleListener listener) {
|
||||||
|
super.addLifecycleListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public LifecycleListener[] findLifecycleListeners() {
|
||||||
|
return super.findLifecycleListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void removeLifecycleListener(LifecycleListener listener) {
|
||||||
|
super.removeLifecycleListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
protected synchronized void startInternal() throws LifecycleException {
|
||||||
|
super.startInternal();
|
||||||
|
super.setState(LifecycleState.STARTING);
|
||||||
|
|
||||||
|
boolean initializedValve = false;
|
||||||
|
for (Valve valve : getContext().getPipeline().getValves()) {
|
||||||
|
if (valve instanceof SessionHandlerValve) {
|
||||||
|
this.handlerValve = (SessionHandlerValve) valve;
|
||||||
|
this.handlerValve.setSessionManager(this);
|
||||||
|
initializedValve = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!initializedValve)
|
||||||
|
throw new LifecycleException("Session handling valve is not initialized..");
|
||||||
|
|
||||||
|
initialize();
|
||||||
|
|
||||||
|
log.info("The sessions will expire after " + (getSessionTimeout()) + " seconds.");
|
||||||
|
getContext().setDistributable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
protected synchronized void stopInternal() throws LifecycleException {
|
||||||
|
super.setState(LifecycleState.STOPPING);
|
||||||
|
super.stopInternal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Session createSession(String sessionId) {
|
||||||
|
if (sessionId != null) {
|
||||||
|
sessionId = (this.dataCache.setnx(sessionId, SessionConstants.NULL_SESSION) == 0L) ? null : sessionId;
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
sessionId = generateSessionId();
|
||||||
|
} while (this.dataCache.setnx(sessionId, SessionConstants.NULL_SESSION) == 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
Session session = (sessionId != null) ? (Session) createEmptySession() : null;
|
||||||
|
if (session != null) {
|
||||||
|
session.setId(sessionId);
|
||||||
|
session.setNew(true);
|
||||||
|
session.setValid(true);
|
||||||
|
session.setCreationTime(System.currentTimeMillis());
|
||||||
|
session.setMaxInactiveInterval(getSessionTimeout());
|
||||||
|
session.tellNew();
|
||||||
|
}
|
||||||
|
setValues(sessionId, session, false, new SessionMetadata());
|
||||||
|
|
||||||
|
if (session != null) {
|
||||||
|
try {
|
||||||
|
save(session, true);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Error occured while creating session..", ex);
|
||||||
|
setValues(null, null);
|
||||||
|
session = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Session createEmptySession() {
|
||||||
|
return new Session(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void add(org.apache.catalina.Session session) {
|
||||||
|
save(session, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public Session findSession(String sessionId) throws IOException {
|
||||||
|
Session session = null;
|
||||||
|
if (sessionId != null && this.sessionContext.get() != null
|
||||||
|
&& sessionId.equals(this.sessionContext.get().getId())) {
|
||||||
|
session = this.sessionContext.get().getSession();
|
||||||
|
} else {
|
||||||
|
byte[] data = this.dataCache.get(sessionId);
|
||||||
|
|
||||||
|
boolean isPersisted = false;
|
||||||
|
SessionMetadata metadata = null;
|
||||||
|
if (data == null) {
|
||||||
|
session = null;
|
||||||
|
metadata = null;
|
||||||
|
sessionId = null;
|
||||||
|
isPersisted = false;
|
||||||
|
} else {
|
||||||
|
if (Arrays.equals(SessionConstants.NULL_SESSION, data)) {
|
||||||
|
throw new IOException("NULL session data");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
metadata = new SessionMetadata();
|
||||||
|
Session newSession = (Session) createEmptySession();
|
||||||
|
this.serializer.deserializeSessionData(data, newSession, metadata);
|
||||||
|
|
||||||
|
newSession.setId(sessionId);
|
||||||
|
newSession.access();
|
||||||
|
newSession.setNew(false);
|
||||||
|
newSession.setValid(true);
|
||||||
|
newSession.resetDirtyTracking();
|
||||||
|
newSession.setMaxInactiveInterval(getSessionTimeout());
|
||||||
|
|
||||||
|
session = newSession;
|
||||||
|
isPersisted = true;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Error occured while de-serializing the session object..", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setValues(sessionId, session, isPersisted, metadata);
|
||||||
|
}
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void remove(org.apache.catalina.Session session) {
|
||||||
|
remove(session, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void remove(org.apache.catalina.Session session, boolean update) {
|
||||||
|
this.dataCache.expire(session.getId(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void load() throws ClassNotFoundException, IOException {
|
||||||
|
// Auto-generated method stub
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@inheritDoc} */
|
||||||
|
@Override
|
||||||
|
public void unload() throws IOException {
|
||||||
|
// Auto-generated method stub
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To initialize the session manager
|
||||||
|
*/
|
||||||
|
private void initialize() {
|
||||||
|
try {
|
||||||
|
this.dataCache = new RedisDataCache();
|
||||||
|
|
||||||
|
this.serializer = new SerializationUtil();
|
||||||
|
ClassLoader loader = (getContext() != null && getContext().getLoader() != null)
|
||||||
|
? getContext().getLoader().getClassLoader()
|
||||||
|
: null;
|
||||||
|
this.serializer.setClassLoader(loader);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Error occured while initializing the session manager..", ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To save session object to data-cache
|
||||||
|
*
|
||||||
|
* @param session
|
||||||
|
* @param forceSave
|
||||||
|
*/
|
||||||
|
public void save(org.apache.catalina.Session session, boolean forceSave) {
|
||||||
|
try {
|
||||||
|
Boolean isPersisted;
|
||||||
|
Session newSession = (Session) session;
|
||||||
|
byte[] hash = (this.sessionContext.get() != null && this.sessionContext.get().getMetadata() != null)
|
||||||
|
? this.sessionContext.get().getMetadata().getAttributesHash()
|
||||||
|
: null;
|
||||||
|
byte[] currentHash = serializer.getSessionAttributesHashCode(newSession);
|
||||||
|
|
||||||
|
if (forceSave || newSession.isDirty()
|
||||||
|
|| (isPersisted = (this.sessionContext.get() != null) ? this.sessionContext.get().isPersisted()
|
||||||
|
: null) == null
|
||||||
|
|| !isPersisted || !Arrays.equals(hash, currentHash)) {
|
||||||
|
|
||||||
|
SessionMetadata metadata = new SessionMetadata();
|
||||||
|
metadata.setAttributesHash(currentHash);
|
||||||
|
|
||||||
|
this.dataCache.set(newSession.getId(), serializer.serializeSessionData(newSession, metadata));
|
||||||
|
newSession.resetDirtyTracking();
|
||||||
|
setValues(true, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
int timeout = getSessionTimeout();
|
||||||
|
this.dataCache.expire(newSession.getId(), timeout);
|
||||||
|
log.trace("Session [" + newSession.getId() + "] expire in [" + timeout + "] seconds.");
|
||||||
|
|
||||||
|
} catch (IOException ex) {
|
||||||
|
log.error("Error occured while saving the session object in data cache..", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To process post request process
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
*/
|
||||||
|
public void afterRequest(Request request) {
|
||||||
|
Session session = null;
|
||||||
|
try {
|
||||||
|
session = (this.sessionContext.get() != null) ? this.sessionContext.get().getSession() : null;
|
||||||
|
if (session != null) {
|
||||||
|
if (session.isValid())
|
||||||
|
save(session, getAlwaysSaveAfterRequest());
|
||||||
|
else
|
||||||
|
remove(session);
|
||||||
|
log.trace("Session object " + (session.isValid() ? "saved: " : "removed: ") + session.getId());
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Error occured while processing post request process..", ex);
|
||||||
|
} finally {
|
||||||
|
this.sessionContext.remove();
|
||||||
|
log.trace("Session removed from ThreadLocal:" + ((session != null) ? session.getIdInternal() : ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private int getSessionTimeout() {
|
||||||
|
int timeout = getContext().getSessionTimeout() * 60;
|
||||||
|
return (timeout < 1800) ? 1800 : timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sessionId
|
||||||
|
* @param session
|
||||||
|
*/
|
||||||
|
private void setValues(String sessionId, Session session) {
|
||||||
|
if (this.sessionContext.get() == null) {
|
||||||
|
this.sessionContext.set(new SessionContext());
|
||||||
|
}
|
||||||
|
this.sessionContext.get().setId(sessionId);
|
||||||
|
this.sessionContext.get().setSession(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param isPersisted
|
||||||
|
* @param metadata
|
||||||
|
*/
|
||||||
|
private void setValues(boolean isPersisted, SessionMetadata metadata) {
|
||||||
|
if (this.sessionContext.get() == null) {
|
||||||
|
this.sessionContext.set(new SessionContext());
|
||||||
|
}
|
||||||
|
this.sessionContext.get().setMetadata(metadata);
|
||||||
|
this.sessionContext.get().setPersisted(isPersisted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param sessionId
|
||||||
|
* @param session
|
||||||
|
* @param isPersisted
|
||||||
|
* @param metadata
|
||||||
|
*/
|
||||||
|
private void setValues(String sessionId, Session session, boolean isPersisted, SessionMetadata metadata) {
|
||||||
|
setValues(sessionId, session);
|
||||||
|
setValues(isPersisted, metadata);
|
||||||
|
}
|
||||||
|
}
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
@ -0,0 +1,41 @@
|
||||||
|
/**
|
||||||
|
* Tomcat clustering with Redis data-cache implementation.
|
||||||
|
*
|
||||||
|
* Tomcat clustering with Redis is the plugable one. It uses to store session objects to Redis data cache.
|
||||||
|
*
|
||||||
|
* @author Ranjith Manickam
|
||||||
|
* @since 2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
Supports:
|
||||||
|
* Apache Tomcat 7
|
||||||
|
* Apache Tomcat 8
|
||||||
|
|
||||||
|
Pre-requisite:
|
||||||
|
1. jedis.jar
|
||||||
|
2. commons-pool2.jar
|
||||||
|
3. commons-logging.jar
|
||||||
|
|
||||||
|
more details.. https://github.com/ran-jit/TomcatClusterRedisSessionManager/wiki
|
||||||
|
|
||||||
|
Steps to be done,
|
||||||
|
1. Move the downloaded jars to tomcat/lib directory
|
||||||
|
* $catalina.home/lib/
|
||||||
|
|
||||||
|
2. Add tomcat system property "catalina.base"
|
||||||
|
* catalina.base="TOMCAT_LOCATION"
|
||||||
|
|
||||||
|
3. Extract downloaded package (tomcat-cluster-redis-session-manager.zip) to configure Redis credentials in redis-data-cache.properties file and move the file to tomcat/conf directory
|
||||||
|
* tomcat/conf/redis-data-cache.properties
|
||||||
|
|
||||||
|
4. Add the below two lines in tomcat/conf/context.xml
|
||||||
|
<Valve className="tomcat.request.session.redis.SessionHandlerValve" />
|
||||||
|
<Manager className="tomcat.request.session.redis.SessionManager" />
|
||||||
|
|
||||||
|
5. Verify the session expiration time (minutes) in tomcat/conf/web.xml
|
||||||
|
<session-config>
|
||||||
|
<session-timeout>60<session-timeout>
|
||||||
|
<session-config>
|
||||||
|
|
||||||
|
Note:
|
||||||
|
* This supports, both redis stand-alone and multiple node cluster based on the redis-data-cache.properties configuration.
|
|
@ -0,0 +1,16 @@
|
||||||
|
#-- Redis data-cache configuration
|
||||||
|
|
||||||
|
#- redis hosts ex: 127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.2:6380, ....
|
||||||
|
redis.hosts=127.0.0.1:6379
|
||||||
|
|
||||||
|
#- redis password (for stand-alone mode)
|
||||||
|
#redis.password=
|
||||||
|
|
||||||
|
#- set true to enable redis cluster mode
|
||||||
|
redis.cluster.enabled=false
|
||||||
|
|
||||||
|
#- redis database (default 0)
|
||||||
|
#redis.database=0
|
||||||
|
|
||||||
|
#- redis connection timeout (default 2000)
|
||||||
|
#redis.timeout=2000
|
|
@ -0,0 +1,154 @@
|
||||||
|
package tomcat.request.session.redis;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.annotation.WebServlet;
|
||||||
|
import javax.servlet.http.HttpServlet;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
|
@WebServlet("/")
|
||||||
|
public class SessionManagerTest extends HttpServlet {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 7464510533820701851L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
|
||||||
|
* response)
|
||||||
|
*/
|
||||||
|
protected void doGet(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
|
||||||
|
String action = request.getParameter("action");
|
||||||
|
action = (action == null) ? "" : action;
|
||||||
|
String responseData = null;
|
||||||
|
|
||||||
|
switch (action.toUpperCase()) {
|
||||||
|
case "SET":
|
||||||
|
responseData = setSessionValues(request.getSession());
|
||||||
|
break;
|
||||||
|
case "GET":
|
||||||
|
responseData = getSessionValues(request.getSession(), action);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
responseData = getActions(request);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendResponse(response, responseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* method to send response
|
||||||
|
*
|
||||||
|
* @param response
|
||||||
|
* @param responseData
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
private void sendResponse(HttpServletResponse response, String responseData) throws IOException {
|
||||||
|
response.setContentType("text/html");
|
||||||
|
response.setStatus(HttpServletResponse.SC_OK);
|
||||||
|
response.getWriter().println(responseData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* method to get actions
|
||||||
|
*
|
||||||
|
* @param request
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private String getActions(HttpServletRequest request) {
|
||||||
|
StringBuffer xml = new StringBuffer();
|
||||||
|
|
||||||
|
xml.append("<!DOCTYPE html>");
|
||||||
|
xml.append("<html>");
|
||||||
|
xml.append("<head>");
|
||||||
|
xml.append("<meta charset='UTF-8'>");
|
||||||
|
xml.append("<title>tomcat-cluster-redis-session-manager-test</title>");
|
||||||
|
xml.append("</head>");
|
||||||
|
xml.append("<body>");
|
||||||
|
xml.append("<h2>tomcat-cluster-redis-session-manager-test</h2>");
|
||||||
|
xml.append("<h4>actions</h4>");
|
||||||
|
|
||||||
|
String url = request.getRequestURL().toString();
|
||||||
|
url = (url.contains("?action=") ? (url.substring(0, url.indexOf("?action="))) : url).concat("?action=");
|
||||||
|
|
||||||
|
xml.append("<a href='");
|
||||||
|
xml.append(url);
|
||||||
|
xml.append("SET");
|
||||||
|
xml.append("'>SET</a>");
|
||||||
|
xml.append("<br><br>");
|
||||||
|
xml.append("<a href='");
|
||||||
|
xml.append(url);
|
||||||
|
xml.append("GET");
|
||||||
|
xml.append("'>GET</a>");
|
||||||
|
|
||||||
|
xml.append("</body>");
|
||||||
|
xml.append("</html>");
|
||||||
|
|
||||||
|
return xml.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* method to set session values
|
||||||
|
*
|
||||||
|
* @param session
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private String setSessionValues(HttpSession session) {
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
session.setAttribute("test-" + i, "test-" + new Date().getTime());
|
||||||
|
}
|
||||||
|
return getSessionValues(session, "SET");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* method to get session values
|
||||||
|
*
|
||||||
|
* @param session
|
||||||
|
* @param action
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private String getSessionValues(HttpSession session, String action) {
|
||||||
|
StringBuffer xml = new StringBuffer();
|
||||||
|
xml.append("<!DOCTYPE html>");
|
||||||
|
xml.append("<html>");
|
||||||
|
xml.append("<head>");
|
||||||
|
xml.append("<meta charset='UTF-8'>");
|
||||||
|
xml.append("<title>tomcat-cluster-redis-session-manager-test</title>");
|
||||||
|
xml.append("</head>");
|
||||||
|
xml.append("<body>");
|
||||||
|
xml.append("<h2>tomcat-cluster-redis-session-manager-test-results</h2>");
|
||||||
|
xml.append("<h4>action: ");
|
||||||
|
xml.append(action);
|
||||||
|
xml.append("</h4>");
|
||||||
|
xml.append("<table width='30%' border='1' style='text-align: center;'>");
|
||||||
|
xml.append("<tr>");
|
||||||
|
xml.append("<th width='50%'>Key</th>");
|
||||||
|
xml.append("<th width='50%'>Value</th>");
|
||||||
|
xml.append("</tr>");
|
||||||
|
|
||||||
|
Enumeration<String> names = session.getAttributeNames();
|
||||||
|
while (names.hasMoreElements()) {
|
||||||
|
String name = names.nextElement();
|
||||||
|
xml.append("<tr>");
|
||||||
|
xml.append("<td>");
|
||||||
|
xml.append(name);
|
||||||
|
xml.append("</td>");
|
||||||
|
xml.append("<td>");
|
||||||
|
xml.append(session.getAttribute(name));
|
||||||
|
xml.append("</td>");
|
||||||
|
xml.append("</tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
xml.append("</table>");
|
||||||
|
xml.append("</body>");
|
||||||
|
xml.append("</html>");
|
||||||
|
|
||||||
|
return xml.toString();
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue