Feature - Tomcat 10 support added. #3322

pull/3332/head^2
Nikita Koksharov 4 years ago
parent b8ca7287d1
commit 35fce14906

@ -17,6 +17,7 @@
<module>redisson-tomcat-7</module>
<module>redisson-tomcat-8</module>
<module>redisson-tomcat-9</module>
<module>redisson-tomcat-10</module>
</modules>
<build>

@ -0,0 +1,88 @@
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.redisson</groupId>
<artifactId>redisson-tomcat</artifactId>
<version>3.14.2-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<artifactId>redisson-tomcat-10</artifactId>
<packaging>jar</packaging>
<name>Redisson/Tomcat-10</name>
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>10.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>10.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>10.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<archive>
<manifestEntries>
<Build-Time>${maven.build.timestamp}</Build-Time>
<Automatic-Module-Name>redisson.tomcat9</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>3.0</version>
<configuration>
<basedir>${basedir}</basedir>
<header>${basedir}/../../header.txt</header>
<quiet>false</quiet>
<failIfMissing>true</failIfMissing>
<aggregate>false</aggregate>
<includes>
<include>src/main/java/org/redisson/</include>
</includes>
<excludes>
<exclude>target/**</exclude>
</excludes>
<useDefaultExcludes>true</useDefaultExcludes>
<mapping>
<java>JAVADOC_STYLE</java>
</mapping>
<strictCheck>true</strictCheck>
<useDefaultMapping>true</useDefaultMapping>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,80 @@
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.tomcat;
import java.io.IOException;
import java.io.Serializable;
import org.redisson.client.protocol.Decoder;
import org.redisson.client.protocol.Encoder;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
/**
*
* @author Nikita Koksharov
*
*/
public class AttributeMessage implements Serializable {
private String sessionId;
private String nodeId;
public AttributeMessage() {
}
public AttributeMessage(String nodeId, String sessionId) {
this.nodeId = nodeId;
this.sessionId = sessionId;
}
public String getSessionId() {
return sessionId;
}
public String getNodeId() {
return nodeId;
}
protected byte[] toByteArray(Encoder encoder, Object value) throws IOException {
if (value == null) {
return null;
}
ByteBuf buf = encoder.encode(value);
try {
return ByteBufUtil.getBytes(buf);
} finally {
buf.release();
}
}
protected Object toObject(Decoder<?> decoder, byte[] value) throws IOException, ClassNotFoundException {
if (value == null) {
return null;
}
ByteBuf buf = Unpooled.wrappedBuffer(value);
try {
return decoder.decode(buf, null);
} finally {
buf.release();
}
}
}

@ -0,0 +1,42 @@
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.tomcat;
import java.util.Set;
/**
*
* @author Nikita Koksharov
*
*/
public class AttributeRemoveMessage extends AttributeMessage {
private Set<String> names;
public AttributeRemoveMessage() {
super();
}
public AttributeRemoveMessage(String nodeId, String sessionId, Set<String> names) {
super(nodeId, sessionId);
this.names = names;
}
public Set<String> getNames() {
return names;
}
}

@ -0,0 +1,50 @@
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.tomcat;
import java.io.IOException;
import org.redisson.client.protocol.Decoder;
import org.redisson.client.protocol.Encoder;
/**
*
* @author Nikita Koksharov
*
*/
public class AttributeUpdateMessage extends AttributeMessage {
private String name;
private byte[] value;
public AttributeUpdateMessage() {
}
public AttributeUpdateMessage(String nodeId, String sessionId, String name, Object value, Encoder encoder) throws IOException {
super(nodeId, sessionId);
this.name = name;
this.value = toByteArray(encoder, value);
}
public String getName() {
return name;
}
public Object getValue(Decoder<?> decoder) throws IOException, ClassNotFoundException {
return toObject(decoder, value);
}
}

@ -0,0 +1,32 @@
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.tomcat;
/**
*
* @author Nikita Koksharov
*
*/
public class AttributesClearMessage extends AttributeMessage {
public AttributesClearMessage() {
}
public AttributesClearMessage(String nodeId, String sessionId) {
super(nodeId, sessionId);
}
}

@ -0,0 +1,61 @@
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.tomcat;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.redisson.client.protocol.Decoder;
import org.redisson.client.protocol.Encoder;
/**
*
* @author Nikita Koksharov
*
*/
public class AttributesPutAllMessage extends AttributeMessage {
private Map<String, byte[]> attrs;
public AttributesPutAllMessage() {
}
public AttributesPutAllMessage(String nodeId, String sessionId, Map<String, Object> attrs, Encoder encoder) throws IOException {
super(nodeId, sessionId);
if (attrs != null) {
this.attrs = new HashMap<String, byte[]>();
for (Entry<String, Object> entry: attrs.entrySet()) {
this.attrs.put(entry.getKey(), toByteArray(encoder, entry.getValue()));
}
} else {
this.attrs = null;
}
}
public Map<String, Object> getAttrs(Decoder<?> decoder) throws IOException, ClassNotFoundException {
if (attrs == null) {
return null;
}
Map<String, Object> result = new HashMap<String, Object>();
for (Entry<String, byte[]> entry: attrs.entrySet()) {
result.put(entry.getKey(), toObject(decoder, entry.getValue()));
}
return result;
}
}

@ -0,0 +1,73 @@
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.tomcat;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import org.apache.catalina.LifecycleException;
import org.redisson.api.RedissonClient;
/**
* Redisson Session Manager for Apache Tomcat.
* Uses Redisson instance located in JNDI.
*
* @author Nikita Koksharov
*
*/
public class JndiRedissonSessionManager extends RedissonSessionManager {
private String jndiName;
@Override
public void setConfigPath(String configPath) {
throw new IllegalArgumentException("configPath is unavaialble for JNDI based manager");
}
@Override
protected RedissonClient buildClient() throws LifecycleException {
InitialContext context = null;
try {
context = new InitialContext();
Context envCtx = (Context) context.lookup("java:comp/env");
return (RedissonClient) envCtx.lookup(jndiName);
} catch (NamingException e) {
throw new LifecycleException("Unable to locate Redisson instance by name: " + jndiName, e);
} finally {
if (context != null) {
try {
context.close();
} catch (NamingException e) {
throw new LifecycleException("Unable to close JNDI context", e);
}
}
}
}
public String getJndiName() {
return jndiName;
}
public void setJndiName(String jndiName) {
this.jndiName = jndiName;
}
@Override
protected void shutdownRedisson() {
}
}

@ -0,0 +1,498 @@
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.tomcat;
import org.apache.catalina.session.StandardSession;
import org.redisson.api.RMap;
import org.redisson.api.RSet;
import org.redisson.api.RTopic;
import org.redisson.tomcat.RedissonSessionManager.ReadMode;
import org.redisson.tomcat.RedissonSessionManager.UpdateMode;
import java.io.IOException;
import java.lang.reflect.Field;
import java.security.Principal;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Redisson Session object for Apache Tomcat
*
* @author Nikita Koksharov
*
*/
public class RedissonSession extends StandardSession {
private static final String IS_NEW_ATTR = "session:isNew";
private static final String IS_VALID_ATTR = "session:isValid";
private static final String THIS_ACCESSED_TIME_ATTR = "session:thisAccessedTime";
private static final String MAX_INACTIVE_INTERVAL_ATTR = "session:maxInactiveInterval";
private static final String LAST_ACCESSED_TIME_ATTR = "session:lastAccessedTime";
private static final String CREATION_TIME_ATTR = "session:creationTime";
private static final String IS_EXPIRATION_LOCKED = "session:isExpirationLocked";
private static final String PRINCIPAL_ATTR = "session:principal";
private static final String AUTHTYPE_ATTR = "session:authtype";
public static final Set<String> ATTRS = new HashSet<String>(Arrays.asList(
IS_NEW_ATTR, IS_VALID_ATTR,
THIS_ACCESSED_TIME_ATTR, MAX_INACTIVE_INTERVAL_ATTR,
LAST_ACCESSED_TIME_ATTR, CREATION_TIME_ATTR, IS_EXPIRATION_LOCKED,
PRINCIPAL_ATTR, AUTHTYPE_ATTR
));
private boolean isExpirationLocked;
private boolean loaded;
private final RedissonSessionManager redissonManager;
private final Map<String, Object> attrs;
private RMap<String, Object> map;
private final RTopic topic;
private final ReadMode readMode;
private final UpdateMode updateMode;
private final AtomicInteger usages = new AtomicInteger();
private Map<String, Object> loadedAttributes = Collections.emptyMap();
private Map<String, Object> updatedAttributes = Collections.emptyMap();
private Set<String> removedAttributes = Collections.emptySet();
private final boolean broadcastSessionEvents;
public RedissonSession(RedissonSessionManager manager, ReadMode readMode, UpdateMode updateMode, boolean broadcastSessionEvents) {
super(manager);
this.redissonManager = manager;
this.readMode = readMode;
this.updateMode = updateMode;
this.topic = redissonManager.getTopic();
this.broadcastSessionEvents = broadcastSessionEvents;
if (updateMode == UpdateMode.AFTER_REQUEST) {
removedAttributes = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
}
if (readMode == ReadMode.REDIS) {
loadedAttributes = new ConcurrentHashMap<>();
updatedAttributes = new ConcurrentHashMap<>();
}
try {
Field attr = StandardSession.class.getDeclaredField("attributes");
attrs = (Map<String, Object>) attr.get(this);
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
private static final long serialVersionUID = -2518607181636076487L;
@Override
public Object getAttribute(String name) {
if (readMode == ReadMode.REDIS) {
if (!isValidInternal()) {
throw new IllegalStateException(sm.getString("standardSession.getAttribute.ise"));
}
if (name == null) {
return null;
}
if (removedAttributes.contains(name)) {
return super.getAttribute(name);
}
Object value = loadedAttributes.get(name);
if (value == null) {
value = map.get(name);
if (value != null) {
loadedAttributes.put(name, value);
}
}
return value;
} else {
if (!loaded) {
synchronized (this) {
if (!loaded) {
Map<String, Object> storedAttrs = map.readAllMap();
load(storedAttrs);
loaded = true;
}
}
}
}
return super.getAttribute(name);
}
@Override
public Enumeration<String> getAttributeNames() {
if (readMode == ReadMode.REDIS) {
if (!isValidInternal()) {
throw new IllegalStateException
(sm.getString("standardSession.getAttributeNames.ise"));
}
Set<String> attributeKeys = new HashSet<>();
attributeKeys.addAll(map.readAllKeySet());
attributeKeys.addAll(loadedAttributes.keySet());
return Collections.enumeration(attributeKeys);
}
return super.getAttributeNames();
}
@Override
public String[] getValueNames() {
if (readMode == ReadMode.REDIS) {
if (!isValidInternal()) {
throw new IllegalStateException
(sm.getString("standardSession.getAttributeNames.ise"));
}
Set<String> keys = map.readAllKeySet();
return keys.toArray(new String[keys.size()]);
}
return super.getValueNames();
}
public void delete() {
if (map == null) {
map = redissonManager.getMap(id);
}
if (broadcastSessionEvents) {
RSet<String> set = redissonManager.getNotifiedNodes(id);
set.add(redissonManager.getNodeId());
set.expire(60, TimeUnit.SECONDS);
map.fastPut(IS_EXPIRATION_LOCKED, true);
map.expire(60, TimeUnit.SECONDS);
} else {
map.delete();
}
if (readMode == ReadMode.MEMORY) {
topic.publish(new AttributesClearMessage(redissonManager.getNodeId(), getId()));
}
map = null;
loadedAttributes.clear();
updatedAttributes.clear();
}
@Override
public void setCreationTime(long time) {
super.setCreationTime(time);
if (map != null) {
Map<String, Object> newMap = new HashMap<String, Object>(3);
newMap.put(CREATION_TIME_ATTR, creationTime);
newMap.put(LAST_ACCESSED_TIME_ATTR, lastAccessedTime);
newMap.put(THIS_ACCESSED_TIME_ATTR, thisAccessedTime);
map.putAll(newMap);
if (readMode == ReadMode.MEMORY) {
topic.publish(createPutAllMessage(newMap));
}
}
}
@Override
public void access() {
super.access();
if (map != null) {
fastPut(THIS_ACCESSED_TIME_ATTR, thisAccessedTime);
expireSession();
}
}
protected void expireSession() {
if (isExpirationLocked) {
return;
}
if (maxInactiveInterval >= 0) {
map.expire(maxInactiveInterval + 60, TimeUnit.SECONDS);
}
}
protected AttributesPutAllMessage createPutAllMessage(Map<String, Object> newMap) {
Map<String, Object> map = new HashMap<String, Object>();
for (Entry<String, Object> entry : newMap.entrySet()) {
map.put(entry.getKey(), entry.getValue());
}
try {
return new AttributesPutAllMessage(redissonManager.getNodeId(), getId(), map, this.map.getCodec().getMapValueEncoder());
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
@Override
public void setMaxInactiveInterval(int interval) {
super.setMaxInactiveInterval(interval);
if (map != null) {
fastPut(MAX_INACTIVE_INTERVAL_ATTR, maxInactiveInterval);
expireSession();
}
}
private void fastPut(String name, Object value) {
map.fastPut(name, value);
if (readMode == ReadMode.MEMORY) {
try {
topic.publish(new AttributeUpdateMessage(redissonManager.getNodeId(), getId(), name, value, this.map.getCodec().getMapValueEncoder()));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
@Override
public void setPrincipal(Principal principal) {
super.setPrincipal(principal);
if (map != null) {
fastPut(PRINCIPAL_ATTR, principal);
}
}
@Override
public void setAuthType(String authType) {
super.setAuthType(authType);
if (map != null && authType != null) {
fastPut(AUTHTYPE_ATTR, authType);
}
}
@Override
public void setValid(boolean isValid) {
super.setValid(isValid);
if (map != null) {
if (!isValid && !map.isExists()) {
return;
}
fastPut(IS_VALID_ATTR, isValid);
}
}
@Override
public void setNew(boolean isNew) {
super.setNew(isNew);
if (map != null) {
fastPut(IS_NEW_ATTR, isNew);
}
}
@Override
public void endAccess() {
boolean oldValue = isNew;
super.endAccess();
if (map != null) {
if (isNew != oldValue) {
fastPut(IS_NEW_ATTR, isNew);
}
Map<String, Object> newMap = new HashMap<>(2);
newMap.put(LAST_ACCESSED_TIME_ATTR, lastAccessedTime);
newMap.put(THIS_ACCESSED_TIME_ATTR, thisAccessedTime);
map.putAll(newMap);
if (readMode == ReadMode.MEMORY) {
topic.publish(createPutAllMessage(newMap));
}
expireSession();
}
}
public void superSetAttribute(String name, Object value, boolean notify) {
super.setAttribute(name, value, notify);
}
@Override
public void setAttribute(String name, Object value, boolean notify) {
super.setAttribute(name, value, notify);
if (value == null) {
return;
}
if (updateMode == UpdateMode.DEFAULT && map != null) {
fastPut(name, value);
}
if (readMode == ReadMode.REDIS) {
loadedAttributes.put(name, value);
updatedAttributes.put(name, value);
}
if (updateMode == UpdateMode.AFTER_REQUEST) {
removedAttributes.remove(name);
}
}
public void superRemoveAttributeInternal(String name, boolean notify) {
super.removeAttributeInternal(name, notify);
}
@Override
public long getIdleTimeInternal() {
long idleTime = super.getIdleTimeInternal();
if (map != null && readMode == ReadMode.REDIS) {
if (idleTime >= getMaxInactiveInterval() * 1000) {
load(map.getAll(RedissonSession.ATTRS));
idleTime = super.getIdleTimeInternal();
}
}
return idleTime;
}
@Override
protected void removeAttributeInternal(String name, boolean notify) {
super.removeAttributeInternal(name, notify);
if (updateMode == UpdateMode.DEFAULT && map != null) {
map.fastRemove(name);
if (readMode == ReadMode.MEMORY) {
topic.publish(new AttributeRemoveMessage(redissonManager.getNodeId(), getId(), new HashSet<String>(Arrays.asList(name))));
}
}
if (readMode == ReadMode.REDIS) {
loadedAttributes.remove(name);
updatedAttributes.remove(name);
}
if (updateMode == UpdateMode.AFTER_REQUEST) {
removedAttributes.add(name);
}
}
public void save() {
if (map == null) {
map = redissonManager.getMap(id);
}
Map<String, Object> newMap = new HashMap<String, Object>();
newMap.put(CREATION_TIME_ATTR, creationTime);
newMap.put(LAST_ACCESSED_TIME_ATTR, lastAccessedTime);
newMap.put(THIS_ACCESSED_TIME_ATTR, thisAccessedTime);
newMap.put(MAX_INACTIVE_INTERVAL_ATTR, maxInactiveInterval);
newMap.put(IS_VALID_ATTR, isValid);
newMap.put(IS_NEW_ATTR, isNew);
if (principal != null) {
newMap.put(PRINCIPAL_ATTR, principal);
}
if (authType != null) {
newMap.put(AUTHTYPE_ATTR, authType);
}
if (broadcastSessionEvents) {
newMap.put(IS_EXPIRATION_LOCKED, isExpirationLocked);
}
if (readMode == ReadMode.MEMORY) {
if (attrs != null) {
for (Entry<String, Object> entry : attrs.entrySet()) {
newMap.put(entry.getKey(), entry.getValue());
}
}
} else {
newMap.putAll(updatedAttributes);
updatedAttributes.clear();
}
map.putAll(newMap);
map.fastRemove(removedAttributes.toArray(new String[0]));
if (readMode == ReadMode.MEMORY) {
topic.publish(createPutAllMessage(newMap));
if (updateMode == UpdateMode.AFTER_REQUEST) {
if (!removedAttributes.isEmpty()) {
topic.publish(new AttributeRemoveMessage(redissonManager.getNodeId(), getId(), new HashSet<>(removedAttributes)));
}
}
}
removedAttributes.clear();
expireSession();
}
public void load(Map<String, Object> attrs) {
Long creationTime = (Long) attrs.remove(CREATION_TIME_ATTR);
if (creationTime != null) {
this.creationTime = creationTime;
}
Long lastAccessedTime = (Long) attrs.remove(LAST_ACCESSED_TIME_ATTR);
if (lastAccessedTime != null) {
this.lastAccessedTime = lastAccessedTime;
}
Integer maxInactiveInterval = (Integer) attrs.remove(MAX_INACTIVE_INTERVAL_ATTR);
if (maxInactiveInterval != null) {
this.maxInactiveInterval = maxInactiveInterval;
}
Long thisAccessedTime = (Long) attrs.remove(THIS_ACCESSED_TIME_ATTR);
if (thisAccessedTime != null) {
this.thisAccessedTime = thisAccessedTime;
}
Boolean isValid = (Boolean) attrs.remove(IS_VALID_ATTR);
if (isValid != null) {
this.isValid = isValid;
}
Boolean isNew = (Boolean) attrs.remove(IS_NEW_ATTR);
if (isNew != null) {
this.isNew = isNew;
}
Boolean isExpirationLocked = (Boolean) attrs.remove(IS_EXPIRATION_LOCKED);
if (isExpirationLocked != null) {
this.isExpirationLocked = isExpirationLocked;
}
Principal p = (Principal) attrs.remove(PRINCIPAL_ATTR);
if (p != null) {
this.principal = p;
}
String authType = (String) attrs.remove(AUTHTYPE_ATTR);
if (authType != null) {
this.authType = authType;
}
if (readMode == ReadMode.MEMORY) {
for (Entry<String, Object> entry : attrs.entrySet()) {
super.setAttribute(entry.getKey(), entry.getValue(), false);
}
}
}
@Override
public void recycle() {
super.recycle();
map = null;
loadedAttributes.clear();
updatedAttributes.clear();
removedAttributes.clear();
}
public void startUsage() {
usages.incrementAndGet();
}
public void endUsage() {
// don't decrement usages if startUsage wasn't called
// if (usages.decrementAndGet() == 0) {
if (usages.get() == 0 || usages.decrementAndGet() == 0) {
loadedAttributes.clear();
}
}
}

@ -0,0 +1,410 @@
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.tomcat;
import jakarta.servlet.http.HttpSession;
import org.apache.catalina.*;
import org.apache.catalina.session.ManagerBase;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.redisson.Redisson;
import org.redisson.api.RMap;
import org.redisson.api.RSet;
import org.redisson.api.RTopic;
import org.redisson.api.RedissonClient;
import org.redisson.api.listener.MessageListener;
import org.redisson.client.codec.Codec;
import org.redisson.client.codec.StringCodec;
import org.redisson.codec.CompositeCodec;
import org.redisson.config.Config;
import java.io.File;
import java.io.IOException;
import java.util.*;
/**
* Redisson Session Manager for Apache Tomcat
*
* @author Nikita Koksharov
*
*/
public class RedissonSessionManager extends ManagerBase {
public enum ReadMode {REDIS, MEMORY}
public enum UpdateMode {DEFAULT, AFTER_REQUEST}
private final Log log = LogFactory.getLog(RedissonSessionManager.class);
private RedissonClient redisson;
private String configPath;
private ReadMode readMode = ReadMode.REDIS;
private UpdateMode updateMode = UpdateMode.DEFAULT;
private String keyPrefix = "";
private boolean broadcastSessionEvents = false;
private final String nodeId = UUID.randomUUID().toString();
private MessageListener messageListener;
private Codec codecToUse;
public String getNodeId() { return nodeId; }
public String getUpdateMode() {
return updateMode.toString();
}
public void setUpdateMode(String updateMode) {
this.updateMode = UpdateMode.valueOf(updateMode);
}
public boolean isBroadcastSessionEvents() {
return broadcastSessionEvents;
}
public void setBroadcastSessionEvents(boolean replicateSessionEvents) {
this.broadcastSessionEvents = replicateSessionEvents;
}
public String getReadMode() {
return readMode.toString();
}
public void setReadMode(String readMode) {
this.readMode = ReadMode.valueOf(readMode);
}
public void setConfigPath(String configPath) {
this.configPath = configPath;
}
public String getConfigPath() {
return configPath;
}
public String getKeyPrefix() {
return keyPrefix;
}
public void setKeyPrefix(String keyPrefix) {
this.keyPrefix = keyPrefix;
}
@Override
public String getName() {
return RedissonSessionManager.class.getSimpleName();
}
@Override
public void load() throws ClassNotFoundException, IOException {
}
@Override
public void unload() throws IOException {
}
@Override
public Session createSession(String sessionId) {
Session session = super.createSession(sessionId);
if (broadcastSessionEvents) {
getTopic().publish(new SessionCreatedMessage(getNodeId(), session.getId()));
session.addSessionListener(new SessionListener() {
@Override
public void sessionEvent(SessionEvent event) {
if (event.getType().equals(Session.SESSION_DESTROYED_EVENT)) {
getTopic().publish(new SessionDestroyedMessage(getNodeId(), session.getId()));
}
}
});
}
return session;
}
public RSet<String> getNotifiedNodes(String sessionId) {
String separator = keyPrefix == null || keyPrefix.isEmpty() ? "" : ":";
String name = keyPrefix + separator + "redisson:tomcat_notified_nodes:" + sessionId;
return redisson.getSet(name, StringCodec.INSTANCE);
}
public RMap<String, Object> getMap(String sessionId) {
String separator = keyPrefix == null || keyPrefix.isEmpty() ? "" : ":";
String name = keyPrefix + separator + "redisson:tomcat_session:" + sessionId;
return redisson.getMap(name, new CompositeCodec(StringCodec.INSTANCE, codecToUse, codecToUse));
}
public RTopic getTopic() {
String separator = keyPrefix == null || keyPrefix.isEmpty() ? "" : ":";
final String name = keyPrefix + separator + "redisson:tomcat_session_updates:" + getContext().getName();
return redisson.getTopic(name);
}
@Override
public Session findSession(String id) throws IOException {
return findSession(id, true);
}
private Session findSession(String id, boolean notify) throws IOException {
Session result = super.findSession(id);
if (result == null) {
if (id != null) {
Map<String, Object> attrs = new HashMap<String, Object>();
try {
attrs = getMap(id).getAll(RedissonSession.ATTRS);
} catch (Exception e) {
log.error("Can't read session object by id: " + id, e);
}
if (attrs.isEmpty() || (broadcastSessionEvents && getNotifiedNodes(id).contains(nodeId))) {
log.info("Session " + id + " can't be found");
return null;
}
RedissonSession session = (RedissonSession) createEmptySession();
session.load(attrs);
session.setId(id, notify);
session.access();
session.endAccess();
return session;
}
return null;
}
result.access();
result.endAccess();
return result;
}
@Override
public Session createEmptySession() {
return new RedissonSession(this, readMode, updateMode, broadcastSessionEvents);
}
@Override
public void remove(Session session, boolean update) {
super.remove(session, update);
if (session.getIdInternal() != null) {
((RedissonSession)session).delete();
}
}
@Override
public void add(Session session) {
super.add(session);
((RedissonSession)session).save();
}
public RedissonClient getRedisson() {
return redisson;
}
@Override
protected void startInternal() throws LifecycleException {
super.startInternal();
redisson = buildClient();
final ClassLoader applicationClassLoader;
if (getContext().getLoader().getClassLoader() != null) {
applicationClassLoader = getContext().getLoader().getClassLoader();
} else if (Thread.currentThread().getContextClassLoader() != null) {
applicationClassLoader = Thread.currentThread().getContextClassLoader();
} else {
applicationClassLoader = getClass().getClassLoader();
}
Codec codec = redisson.getConfig().getCodec();
try {
codecToUse = codec.getClass()
.getConstructor(ClassLoader.class, codec.getClass())
.newInstance(applicationClassLoader, codec);
} catch (Exception e) {
throw new LifecycleException(e);
}
Pipeline pipeline = getContext().getPipeline();
synchronized (pipeline) {
if (readMode == ReadMode.REDIS) {
Optional<Valve> res = Arrays.stream(pipeline.getValves()).filter(v -> v.getClass() == UsageValve.class).findAny();
if (res.isPresent()) {
((UsageValve)res.get()).incUsage();
} else {
pipeline.addValve(new UsageValve());
}
}
if (updateMode == UpdateMode.AFTER_REQUEST) {
Optional<Valve> res = Arrays.stream(pipeline.getValves()).filter(v -> v.getClass() == UpdateValve.class).findAny();
if (res.isPresent()) {
((UpdateValve)res.get()).incUsage();
} else {
pipeline.addValve(new UpdateValve());
}
}
}
if (readMode == ReadMode.MEMORY || broadcastSessionEvents) {
RTopic updatesTopic = getTopic();
messageListener = new MessageListener<AttributeMessage>() {
@Override
public void onMessage(CharSequence channel, AttributeMessage msg) {
try {
if (msg.getNodeId().equals(nodeId)) {
return;
}
RedissonSession session = (RedissonSession) RedissonSessionManager.super.findSession(msg.getSessionId());
if (session != null) {
if (msg instanceof SessionDestroyedMessage) {
session.expire();
}
if (msg instanceof AttributeRemoveMessage) {
for (String name : ((AttributeRemoveMessage)msg).getNames()) {
session.superRemoveAttributeInternal(name, true);
}
}
if (msg instanceof AttributesClearMessage) {
RedissonSessionManager.super.remove(session, false);
}
if (msg instanceof AttributesPutAllMessage) {
AttributesPutAllMessage m = (AttributesPutAllMessage) msg;
Map<String, Object> attrs = m.getAttrs(codecToUse.getMapValueDecoder());
session.load(attrs);
}
if (msg instanceof AttributeUpdateMessage) {
AttributeUpdateMessage m = (AttributeUpdateMessage)msg;
session.superSetAttribute(m.getName(), m.getValue(codecToUse.getMapValueDecoder()), true);
}
} else {
if (msg instanceof SessionCreatedMessage) {
Session s = findSession(msg.getSessionId());
if (s == null) {
throw new IllegalStateException("Unable to find session: " + msg.getSessionId());
}
}
if (msg instanceof SessionDestroyedMessage) {
Session s = findSession(msg.getSessionId(), false);
if (s == null) {
throw new IllegalStateException("Unable to find session: " + msg.getSessionId());
}
s.expire();
RSet<String> set = getNotifiedNodes(msg.getSessionId());
set.add(nodeId);
}
}
} catch (Exception e) {
log.error("Unable to handle topic message", e);
}
}
};
updatesTopic.addListener(AttributeMessage.class, messageListener);
}
setState(LifecycleState.STARTING);
}
protected RedissonClient buildClient() throws LifecycleException {
Config config = null;
try {
config = Config.fromYAML(new File(configPath), getClass().getClassLoader());
} catch (IOException e) {
// trying next format
try {
config = Config.fromJSON(new File(configPath), getClass().getClassLoader());
} catch (IOException e1) {
log.error("Can't parse json config " + configPath, e);
throw new LifecycleException("Can't parse yaml config " + configPath, e1);
}
}
try {
return Redisson.create(config);
} catch (Exception e) {
throw new LifecycleException(e);
}
}
@Override
protected void stopInternal() throws LifecycleException {
super.stopInternal();
setState(LifecycleState.STOPPING);
Pipeline pipeline = getContext().getPipeline();
synchronized (pipeline) {
if (readMode == ReadMode.REDIS) {
Arrays.stream(pipeline.getValves()).filter(v -> v.getClass() == UsageValve.class).forEach(v -> {
if (((UsageValve)v).decUsage() == 0){
pipeline.removeValve(v);
}
});
}
if (updateMode == UpdateMode.AFTER_REQUEST) {
Arrays.stream(pipeline.getValves()).filter(v -> v.getClass() == UpdateValve.class).forEach(v -> {
if (((UpdateValve)v).decUsage() == 0){
pipeline.removeValve(v);
}
});
}
}
if (messageListener != null) {
RTopic updatesTopic = getTopic();
updatesTopic.removeListener(messageListener);
}
codecToUse = null;
try {
shutdownRedisson();
} catch (Exception e) {
throw new LifecycleException(e);
}
}
protected void shutdownRedisson() {
if (redisson != null) {
redisson.shutdown();
}
}
public void store(HttpSession session) throws IOException {
if (session == null) {
return;
}
RedissonSession sess = (RedissonSession) super.findSession(session.getId());
if (sess != null) {
sess.access();
sess.endAccess();
sess.save();
}
}
}

@ -0,0 +1,32 @@
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.tomcat;
/**
*
* @author Nikita Koksharov
*
*/
public class SessionCreatedMessage extends AttributeMessage {
public SessionCreatedMessage() {
}
public SessionCreatedMessage(String nodeId, String sessionId) {
super(nodeId, sessionId);
}
}

@ -0,0 +1,32 @@
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.tomcat;
/**
*
* @author Nikita Koksharov
*
*/
public class SessionDestroyedMessage extends AttributeMessage {
public SessionDestroyedMessage() {
}
public SessionDestroyedMessage(String nodeId, String sessionId) {
super(nodeId, sessionId);
}
}

@ -0,0 +1,83 @@
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.tomcat;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import jakarta.servlet.ServletException;
import org.apache.catalina.Manager;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
/**
* Redisson Valve object for Apache Tomcat
*
* @author Nikita Koksharov
*
*/
public class UpdateValve extends ValveBase {
private static final String ALREADY_FILTERED_NOTE = UpdateValve.class.getName() + ".ALREADY_FILTERED_NOTE";
private final AtomicInteger usage = new AtomicInteger(1);
public UpdateValve() {
super(true);
}
public void incUsage() {
usage.incrementAndGet();
}
public int decUsage() {
return usage.decrementAndGet();
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
if (getNext() == null) {
return;
}
//check if we already filtered/processed this request
if (request.getNote(ALREADY_FILTERED_NOTE) == null) {
request.setNote(ALREADY_FILTERED_NOTE, Boolean.TRUE);
try {
getNext().invoke(request, response);
} finally {
request.removeNote(ALREADY_FILTERED_NOTE);
if (request.getContext() == null) {
return;
}
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
try {
ClassLoader applicationClassLoader = request.getContext().getLoader().getClassLoader();
Thread.currentThread().setContextClassLoader(applicationClassLoader);
Manager manager = request.getContext().getManager();
((RedissonSessionManager)manager).store(request.getSession(false));
} finally {
Thread.currentThread().setContextClassLoader(classLoader);
}
}
} else {
getNext().invoke(request, response);
}
}
}

@ -0,0 +1,89 @@
/**
* Copyright (c) 2013-2020 Nikita Koksharov
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.redisson.tomcat;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpSession;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Redisson Valve object for Apache Tomcat
*
* @author Nikita Koksharov
*
*/
public class UsageValve extends ValveBase {
private static final String ALREADY_FILTERED_NOTE = UsageValve.class.getName() + ".ALREADY_FILTERED_NOTE";
private final AtomicInteger usage = new AtomicInteger(1);
public UsageValve() {
super(true);
}
public void incUsage() {
usage.incrementAndGet();
}
public int decUsage() {
return usage.decrementAndGet();
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
if (getNext() == null) {
return;
}
//check if we already filtered/processed this request
if (request.getNote(ALREADY_FILTERED_NOTE) == null) {
request.setNote(ALREADY_FILTERED_NOTE, Boolean.TRUE);
try {
if (request.getContext() != null) {
HttpSession session = request.getSession(false);
if (session != null) {
RedissonSession s = (RedissonSession) request.getContext().getManager().findSession(session.getId());
if (s != null) {
s.startUsage();
}
}
}
getNext().invoke(request, response);
} finally {
request.removeNote(ALREADY_FILTERED_NOTE);
if (request.getContext() != null) {
HttpSession session = request.getSession(false);
if (session != null) {
RedissonSession s = (RedissonSession) request.getContext().getManager().findSession(session.getId());
if (s != null) {
s.endUsage();
}
}
}
}
} else {
getNext().invoke(request, response);
}
}
}

@ -0,0 +1,333 @@
package org.redisson.tomcat;
import org.apache.http.client.fluent.Executor;
import org.apache.http.client.fluent.Request;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
@RunWith(Parameterized.class)
public class RedissonSessionManagerTest {
@Parameterized.Parameters(name= "{index} - {0}")
public static Iterable<Object[]> data() {
return Arrays.asList(new Object[][] {
{"context_memory.xml"},
{"context_redis.xml"},
{"context_redis_after_request.xml"},
{"context_memory_after_request.xml"}
});
}
@Parameterized.Parameter(0)
public String contextName;
@Before
public void before() throws IOException {
String basePath = "src/test/webapp/META-INF/";
Files.deleteIfExists(Paths.get(basePath + "context.xml"));
Files.copy(Paths.get(basePath + contextName), Paths.get(basePath + "context.xml"));
}
@Test
public void testUpdateTwoServers_readValue() throws Exception {
TomcatServer server1 = new TomcatServer("myapp", 8080, "src/test/");
TomcatServer server2 = new TomcatServer("myapp", 8081, "src/test/");
try {
server1.start();
server2.start();
Executor executor = Executor.newInstance();
BasicCookieStore cookieStore = new BasicCookieStore();
executor.use(cookieStore);
write(8080, executor, "test", "from_server1");
write(8081, executor, "test", "from_server2");
read(8080, executor, "test", "from_server2");
read(8080, executor, "test", "from_server2");
} finally {
Executor.closeIdleConnections();
server1.stop();
server2.stop();
}
}
@Test
public void testHttpSessionListener() throws Exception {
TomcatServer server1 = new TomcatServer("myapp", 8080, "src/test/");
server1.start();
TomcatServer server2 = new TomcatServer("myapp", 8081, "src/test/");
server2.start();
Executor executor = Executor.newInstance();
BasicCookieStore cookieStore = new BasicCookieStore();
executor.use(cookieStore);
TestHttpSessionListener.CREATED_INVOCATION_COUNTER = 0;
TestHttpSessionListener.DESTROYED_INVOCATION_COUNTER = 0;
write(8080, executor, "test", "1234");
TomcatServer server3 = new TomcatServer("myapp", 8082, "src/test/");
server3.start();
invalidate(executor);
Thread.sleep(500);
Assert.assertEquals(2, TestHttpSessionListener.CREATED_INVOCATION_COUNTER);
Assert.assertEquals(3, TestHttpSessionListener.DESTROYED_INVOCATION_COUNTER);
Executor.closeIdleConnections();
server1.stop();
server2.stop();
server3.stop();
}
@Test
public void testUpdateTwoServers_twoValues() throws Exception {
TomcatServer server1 = new TomcatServer("myapp", 8080, "src/test/");
TomcatServer server2 = new TomcatServer("myapp", 8081, "src/test/");
try {
server1.start();
server2.start();
Executor executor = Executor.newInstance();
BasicCookieStore cookieStore = new BasicCookieStore();
executor.use(cookieStore);
write(8080, executor, "key1", "value1");
write(8080, executor, "key2", "value1");
write(8081, executor, "key1", "value2");
write(8081, executor, "key2", "value2");
read(8080, executor, "key1", "value2");
read(8080, executor, "key2", "value2");
} finally {
Executor.closeIdleConnections();
server1.stop();
server2.stop();
}
}
@Test
public void testUpdateTwoServers() throws Exception {
TomcatServer server1 = new TomcatServer("myapp", 8080, "src/test/");
server1.start();
TomcatServer server2 = new TomcatServer("myapp", 8081, "src/test/");
server2.start();
Executor executor = Executor.newInstance();
BasicCookieStore cookieStore = new BasicCookieStore();
executor.use(cookieStore);
write(8080, executor, "test", "1234");
read(8081, executor, "test", "1234");
read(8080, executor, "test", "1234");
write(8080, executor, "test", "324");
read(8081, executor, "test", "324");
Executor.closeIdleConnections();
server1.stop();
server2.stop();
}
@Test
public void testExpiration() throws Exception {
TomcatServer server1 = new TomcatServer("myapp", 8080, "src/test/");
server1.start();
Executor executor = Executor.newInstance();
BasicCookieStore cookieStore = new BasicCookieStore();
executor.use(cookieStore);
write(8080, executor, "test", "1234");
TomcatServer server2 = new TomcatServer("myapp", 8081, "src/test/");
server2.start();
Thread.sleep(30000);
read(8081, executor, "test", "1234");
Thread.sleep(40000);
executor.use(cookieStore);
read(8080, executor, "test", "1234");
Executor.closeIdleConnections();
server1.stop();
server2.stop();
}
@Test
public void testSwitchServer() throws Exception {
// start the server at http://localhost:8080/myapp
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
server.start();
Executor executor = Executor.newInstance();
BasicCookieStore cookieStore = new BasicCookieStore();
executor.use(cookieStore);
write(8080, executor, "test", "1234");
System.out.println("done1");
Cookie cookie = cookieStore.getCookies().get(0);
Executor.closeIdleConnections();
server.stop();
server = new TomcatServer("myapp", 8080, "src/test/");
server.start();
executor = Executor.newInstance();
cookieStore = new BasicCookieStore();
cookieStore.addCookie(cookie);
executor.use(cookieStore);
read(8080, executor, "test", "1234");
remove(executor, "test", "null");
Executor.closeIdleConnections();
server.stop();
}
@Test
public void testWriteReadRemove() throws Exception {
// start the server at http://localhost:8080/myapp
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
server.start();
Executor executor = Executor.newInstance();
write(8080, executor, "test", "1234");
read(8080, executor, "test", "1234");
remove(executor, "test", "null");
Executor.closeIdleConnections();
server.stop();
}
@Test
public void testRecreate() throws Exception {
// start the server at http://localhost:8080/myapp
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
server.start();
Executor executor = Executor.newInstance();
write(8080, executor, "test", "1");
recreate(executor, "test", "2");
read(8080, executor, "test", "2");
Executor.closeIdleConnections();
server.stop();
}
@Test
public void testUpdate() throws Exception {
// start the server at http://localhost:8080/myapp
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
server.start();
Executor executor = Executor.newInstance();
write(8080, executor, "test", "1");
read(8080, executor, "test", "1");
write(8080, executor, "test", "2");
read(8080, executor, "test", "2");
Executor.closeIdleConnections();
server.stop();
}
@Test
public void testInvalidate() throws Exception {
File f = Paths.get("").toAbsolutePath().resolve("src/test/webapp/WEB-INF/redisson.yaml").toFile();
Config config = Config.fromYAML(f);
RedissonClient r = Redisson.create(config);
r.getKeys().flushall();
// start the server at http://localhost:8080/myapp
TomcatServer server = new TomcatServer("myapp", 8080, "src/test/");
server.start();
Executor executor = Executor.newInstance();
BasicCookieStore cookieStore = new BasicCookieStore();
executor.use(cookieStore);
write(8080, executor, "test", "1234");
Cookie cookie = cookieStore.getCookies().get(0);
invalidate(executor);
Executor.closeIdleConnections();
executor = Executor.newInstance();
cookieStore = new BasicCookieStore();
cookieStore.addCookie(cookie);
executor.use(cookieStore);
read(8080, executor, "test", "null");
invalidate(executor);
Executor.closeIdleConnections();
server.stop();
TimeUnit.SECONDS.sleep(60);
Assert.assertEquals(0, r.getKeys().count());
}
private void write(int port, Executor executor, String key, String value) throws IOException {
String url = "http://localhost:" + port + "/myapp/write?key=" + key + "&value=" + value;
String response = executor.execute(Request.Get(url)).returnContent().asString();
Assert.assertEquals("OK", response);
}
private void read(int port, Executor executor, String key, String value) throws IOException {
String url = "http://localhost:" + port + "/myapp/read?key=" + key;
String response = executor.execute(Request.Get(url)).returnContent().asString();
Assert.assertEquals(value, response);
}
private void remove(Executor executor, String key, String value) throws IOException {
String url = "http://localhost:8080/myapp/remove?key=" + key;
String response = executor.execute(Request.Get(url)).returnContent().asString();
Assert.assertEquals(value, response);
}
private void invalidate(Executor executor) throws IOException {
String url = "http://localhost:8080/myapp/invalidate";
String response = executor.execute(Request.Get(url)).returnContent().asString();
Assert.assertEquals("OK", response);
}
private void recreate(Executor executor, String key, String value) throws IOException {
String url = "http://localhost:8080/myapp/recreate?key=" + key + "&value=" + value;
String response = executor.execute(Request.Get(url)).returnContent().asString();
Assert.assertEquals("OK", response);
}
}

@ -0,0 +1,21 @@
package org.redisson.tomcat;
import jakarta.servlet.http.HttpSessionEvent;
import jakarta.servlet.http.HttpSessionListener;
public class TestHttpSessionListener implements HttpSessionListener {
public static int CREATED_INVOCATION_COUNTER;
public static int DESTROYED_INVOCATION_COUNTER;
@Override
public void sessionCreated(HttpSessionEvent se) {
CREATED_INVOCATION_COUNTER++;
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
DESTROYED_INVOCATION_COUNTER++;
}
}

@ -0,0 +1,95 @@
package org.redisson.tomcat;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import java.io.IOException;
public class TestServlet extends HttpServlet {
private static final long serialVersionUID = 1243830648280853203L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
HttpSession session = req.getSession();
if (req.getPathInfo().equals("/write")) {
String[] params = req.getQueryString().split("&");
String key = null;
String value = null;
for (String param : params) {
String[] paramLine = param.split("=");
String keyParam = paramLine[0];
String valueParam = paramLine[1];
if ("key".equals(keyParam)) {
key = valueParam;
}
if ("value".equals(keyParam)) {
value = valueParam;
}
}
session.setAttribute(key, value);
resp.getWriter().print("OK");
} else if (req.getPathInfo().equals("/read")) {
String[] params = req.getQueryString().split("&");
String key = null;
for (String param : params) {
String[] line = param.split("=");
String keyParam = line[0];
if ("key".equals(keyParam)) {
key = line[1];
}
}
Object attr = session.getAttribute(key);
resp.getWriter().print(attr);
} else if (req.getPathInfo().equals("/remove")) {
String[] params = req.getQueryString().split("&");
String key = null;
for (String param : params) {
String[] line = param.split("=");
String keyParam = line[0];
if ("key".equals(keyParam)) {
key = line[1];
}
}
session.removeAttribute(key);
resp.getWriter().print(String.valueOf(session.getAttribute(key)));
} else if (req.getPathInfo().equals("/invalidate")) {
session.invalidate();
resp.getWriter().print("OK");
} else if (req.getPathInfo().equals("/recreate")) {
session.invalidate();
session = req.getSession();
String[] params = req.getQueryString().split("&");
String key = null;
String value = null;
for (String param : params) {
String[] paramLine = param.split("=");
String keyParam = paramLine[0];
String valueParam = paramLine[1];
if ("key".equals(keyParam)) {
key = valueParam;
}
if ("value".equals(keyParam)) {
value = valueParam;
}
}
session.setAttribute(key, value);
resp.getWriter().print("OK");
}
}
}

@ -0,0 +1,66 @@
package org.redisson.tomcat;
import java.net.MalformedURLException;
import jakarta.servlet.ServletException;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TomcatServer {
private Tomcat tomcat = new Tomcat();
private int port;
private boolean isRunning;
private static final Logger LOG = LoggerFactory.getLogger(TomcatServer.class);
private static final boolean isInfo = LOG.isInfoEnabled();
public TomcatServer(String contextPath, int port, String appBase) throws MalformedURLException, ServletException {
if(contextPath == null || appBase == null || appBase.length() == 0) {
throw new IllegalArgumentException("Context path or appbase should not be null");
}
if(!contextPath.startsWith("/")) {
contextPath = "/" + contextPath;
}
tomcat.setBaseDir("."); // location where temp dir is created
tomcat.setPort(port);
tomcat.getHost().setAppBase(".");
tomcat.addWebapp(contextPath, appBase + "/webapp");
}
/**
* Start the tomcat embedded server
* @throws InterruptedException
*/
public void start() throws LifecycleException, InterruptedException {
tomcat.start();
tomcat.getConnector();
isRunning = true;
}
/**
* Stop the tomcat embedded server
*/
public void stop() throws LifecycleException {
if(!isRunning) {
LOG.warn("Tomcat server is not running @ port={}", port);
return;
}
if(isInfo) LOG.info("Stopping the Tomcat server");
tomcat.stop();
tomcat.destroy();
tomcat.getServer().await();
isRunning = false;
}
public boolean isRunning() {
return isRunning;
}
}

@ -0,0 +1,10 @@
<?xml version='1.0' encoding='utf-8'?>
<Context>
<Manager className="org.redisson.tomcat.RedissonSessionManager"
configPath="${catalina.base}/src/test/webapp/WEB-INF/redisson.yaml"
readMode="MEMORY"
updateMode="AFTER_REQUEST"
broadcastSessionEvents="true"/>
</Context>

@ -0,0 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<Context>
<Manager className="org.redisson.tomcat.RedissonSessionManager"
configPath="${catalina.base}/src/test/webapp/WEB-INF/redisson.yaml"
readMode="MEMORY"
broadcastSessionEvents="true"/>
</Context>

@ -0,0 +1,10 @@
<?xml version='1.0' encoding='utf-8'?>
<Context>
<Manager className="org.redisson.tomcat.RedissonSessionManager"
configPath="${catalina.base}/src/test/webapp/WEB-INF/redisson.yaml"
readMode="MEMORY"
updateMode="AFTER_REQUEST"
broadcastSessionEvents="true"/>
</Context>

@ -0,0 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<Context>
<Manager className="org.redisson.tomcat.RedissonSessionManager"
configPath="${catalina.base}/src/test/webapp/WEB-INF/redisson.yaml"
readMode="REDIS"
broadcastSessionEvents="true"/>
</Context>

@ -0,0 +1,10 @@
<?xml version='1.0' encoding='utf-8'?>
<Context>
<Manager className="org.redisson.tomcat.RedissonSessionManager"
configPath="${catalina.base}/src/test/webapp/WEB-INF/redisson.yaml"
readMode="REDIS"
updateMode="AFTER_REQUEST"
broadcastSessionEvents="true"/>
</Context>

@ -0,0 +1,2 @@
singleServerConfig:
address: "redis://127.0.0.1:6379"

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<servlet>
<servlet-name>testServlet</servlet-name>
<servlet-class>org.redisson.tomcat.TestServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>testServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<listener>
<listener-class>org.redisson.tomcat.TestHttpSessionListener</listener-class>
</listener>
</web-app>
Loading…
Cancel
Save