diff --git a/pom.xml b/pom.xml index 387e360..225eaf8 100644 --- a/pom.xml +++ b/pom.xml @@ -194,13 +194,13 @@ org.apache.tomcat tomcat-catalina - 8.5.35 + 8.5.41 provided org.mongodb mongo-java-driver - 3.5.0 + 3.10.2 provided @@ -224,8 +224,13 @@ org.appng appng-api - 1.14.4 + 1.18.0 provided + + + com.hazelcast + hazelcast-client + 3.12 diff --git a/src/main/java/org/appng/tomcat/session/hazelcast/HazelcastPersistentManager.java b/src/main/java/org/appng/tomcat/session/hazelcast/HazelcastPersistentManager.java new file mode 100644 index 0000000..b4a7323 --- /dev/null +++ b/src/main/java/org/appng/tomcat/session/hazelcast/HazelcastPersistentManager.java @@ -0,0 +1,83 @@ +package org.appng.tomcat.session.hazelcast; + +import java.io.IOException; + +import org.apache.catalina.Container; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.Session; +import org.apache.catalina.session.PersistentManagerBase; +import org.apache.juli.logging.Log; +import org.appng.tomcat.session.Utils; + +public class HazelcastPersistentManager extends PersistentManagerBase { + + private final Log log = Utils.getLog(HazelcastSessionTrackerValve.class); + + private String name; + + protected void destroyInternal() throws LifecycleException { + super.destroyInternal(); + } + + @Override + public void add(Session session) { + // do nothing, we don't want to use Map sessions! + } + + @Override + public void remove(Session session, boolean update) { + // avoid super.remove + removeSession(session.getId()); + } + + @Override + public Session findSession(String id) throws IOException { + // do not call super, instead load the session directly from the store + try { + log.info("Manager FIND: " + id); + return getStore().load(id); + } catch (ClassNotFoundException e) { + throw new IOException("error loading class for session " + id, e); + } + } + + @Override + protected String generateSessionId() { + String sessionId = super.generateSessionId(); + log.info("Manager CREATED: " + sessionId); + return sessionId; + } + + @Override + public void removeSuper(Session session) { + log.info("Manager REMOVE SUPER: " + session.getId()); + super.removeSuper(session); + } + + @Override + public String getName() { + if (this.name == null) { + Container container = getContext(); + + String contextName = container.getName(); + if (!contextName.startsWith("/")) { + contextName = "/" + contextName; + } + + String hostName = ""; + String engineName = ""; + + if (container.getParent() != null) { + Container host = container.getParent(); + hostName = host.getName(); + if (host.getParent() != null) { + engineName = host.getParent().getName(); + } + } + + this.name = "/" + engineName + "/" + hostName + contextName; + } + return this.name; + } + +} diff --git a/src/main/java/org/appng/tomcat/session/hazelcast/HazelcastSession.java b/src/main/java/org/appng/tomcat/session/hazelcast/HazelcastSession.java new file mode 100644 index 0000000..f9163d0 --- /dev/null +++ b/src/main/java/org/appng/tomcat/session/hazelcast/HazelcastSession.java @@ -0,0 +1,70 @@ +package org.appng.tomcat.session.hazelcast; + +import java.io.IOException; + +import com.hazelcast.nio.ObjectDataInput; +import com.hazelcast.nio.ObjectDataOutput; +import com.hazelcast.nio.serialization.DataSerializable; + +public class HazelcastSession implements DataSerializable { + private String id; + private String application; + private byte[] data; + private Long created; + private Long lastModified; + + public void writeData(ObjectDataOutput out) throws IOException { + out.writeUTF(id); + out.writeByteArray(data); + out.writeObject(created); + out.writeObject(lastModified); + } + + public void readData(ObjectDataInput in) throws IOException { + id = in.readUTF(); + data = in.readByteArray(); + created = in.readObject(); + lastModified = in.readObject(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getApplication() { + return application; + } + + public void setApplication(String application) { + this.application = application; + } + + public byte[] getData() { + return data; + } + + public void setData(byte[] data) { + this.data = data; + } + + public Long getCreated() { + return created; + } + + public void setCreated(Long created) { + this.created = created; + } + + public Long getLastModified() { + return lastModified; + } + + public void setLastModified(Long lastModified) { + this.lastModified = lastModified; + } + +} diff --git a/src/main/java/org/appng/tomcat/session/hazelcast/HazelcastSessionTrackerValve.java b/src/main/java/org/appng/tomcat/session/hazelcast/HazelcastSessionTrackerValve.java new file mode 100644 index 0000000..d5aaa45 --- /dev/null +++ b/src/main/java/org/appng/tomcat/session/hazelcast/HazelcastSessionTrackerValve.java @@ -0,0 +1,64 @@ +/* + * Copyright 2015-2017 the original author or authors. + * + * 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.appng.tomcat.session.hazelcast; + +import java.io.IOException; + +import javax.servlet.ServletException; + +import org.apache.catalina.Session; +import org.apache.catalina.Valve; +import org.apache.catalina.connector.Request; +import org.apache.catalina.connector.Response; +import org.apache.catalina.valves.PersistentValve; +import org.apache.juli.logging.Log; +import org.appng.tomcat.session.Utils; + +/** + * A {@link Valve} that uses {@link HazelcastPersistentManager} to store a + * {@link Session} + */ +public class HazelcastSessionTrackerValve extends PersistentValve { + + private final Log log = Utils.getLog(HazelcastSessionTrackerValve.class); + + @Override + public void invoke(Request request, Response response) throws IOException, ServletException { + try { + getNext().invoke(request, response); + } finally { + long start = System.currentTimeMillis(); + HazelcastPersistentManager manager = (HazelcastPersistentManager) request.getContext().getManager(); + Session session = request.getSessionInternal(false); + if (session != null) { + if (!Utils.isTemplateRequest(request)) { + if (session.isValid()) { + log.debug(String.format("Request with session completed, saving session %s", session.getId())); + manager.getStore().save(session); + } else { + log.debug(String.format("HTTP Session has been invalidated, removing %s", session.getId())); + manager.remove(session); + } + } + } + long duration = System.currentTimeMillis() - start; + if (log.isDebugEnabled() && duration > 0) { + log.debug(String.format("handling session for %s took %sms", request.getServletPath(), duration)); + } + } + } + +} diff --git a/src/main/java/org/appng/tomcat/session/hazelcast/HazelcastStore.java b/src/main/java/org/appng/tomcat/session/hazelcast/HazelcastStore.java new file mode 100644 index 0000000..a6a206b --- /dev/null +++ b/src/main/java/org/appng/tomcat/session/hazelcast/HazelcastStore.java @@ -0,0 +1,186 @@ +package org.appng.tomcat.session.hazelcast; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.apache.catalina.Session; +import org.apache.catalina.session.StandardSession; +import org.apache.catalina.session.StoreBase; +import org.apache.commons.lang3.StringUtils; +import org.apache.juli.logging.Log; +import org.appng.tomcat.session.Utils; + +import com.hazelcast.client.HazelcastClient; +import com.hazelcast.client.config.ClientConfig; +import com.hazelcast.config.Config; +import com.hazelcast.config.JoinConfig; +import com.hazelcast.config.MulticastConfig; +import com.hazelcast.config.NetworkConfig; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.core.ReplicatedMap; + +public class HazelcastStore extends StoreBase { + + private final Log log = Utils.getLog(HazelcastStore.class); + private HazelcastInstance instance; + + private String mapName = "tomcat.sessions"; + private String mode = "multicast"; + private String group = "dev"; + private String addresses = "localhost:5701"; + private int port = NetworkConfig.DEFAULT_PORT; + + private String multicastGroup = MulticastConfig.DEFAULT_MULTICAST_GROUP; + private int multicastPort = MulticastConfig.DEFAULT_MULTICAST_PORT; + private int multicastTimeoutSeconds = MulticastConfig.DEFAULT_MULTICAST_TIMEOUT_SECONDS; + private int multicastTtl = MulticastConfig.DEFAULT_MULTICAST_TTL; + + private String tcpMembers = "localhost:5701"; + + @Override + protected void initInternal() { + super.initInternal(); + Config config = new Config(); + config.getNetworkConfig().setPort(port); + JoinConfig joinConfig = config.getNetworkConfig().getJoin(); + switch (mode) { + case "client": + ClientConfig clientConfig = new ClientConfig(); + clientConfig.getGroupConfig().setName(group); + String[] addressArr = addresses.split(","); + for (String address : addressArr) { + clientConfig.getNetworkConfig().addAddress(address.trim()); + } + instance = HazelcastClient.newHazelcastClient(clientConfig); + break; + + case "tcp": + joinConfig.getTcpIpConfig().setEnabled(true); + joinConfig.getMulticastConfig().setEnabled(false); + joinConfig.getTcpIpConfig().addMember(tcpMembers); + break; + + default: + joinConfig.getTcpIpConfig().setEnabled(false); + joinConfig.getMulticastConfig().setEnabled(true); + joinConfig.getMulticastConfig().setMulticastGroup(multicastGroup); + joinConfig.getMulticastConfig().setMulticastPort(multicastPort); + joinConfig.getMulticastConfig().setMulticastTimeoutSeconds(multicastTimeoutSeconds); + joinConfig.getMulticastConfig().setMulticastTimeToLive(multicastTtl); + break; + } + instance = Hazelcast.newHazelcastInstance(config); + } + + public void save(Session session) throws IOException { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos)) { + + ((StandardSession) session).writeObjectData(oos); + + byte[] data = bos.toByteArray(); + + HazelcastSession hzSession = new HazelcastSession(); + hzSession.setId(session.getIdInternal()); + hzSession.setApplication(getManager().getName()); + hzSession.setCreated(session.getCreationTime()); + hzSession.setData(data); + hzSession.setLastModified(session.getLastAccessedTimeInternal()); + getMap().put(getManager().getName() + "_" + session.getId(), hzSession); + log.info("saved: " + session.getId() + " (" + session + ")"); + } + } + + @Override + public HazelcastPersistentManager getManager() { + return (HazelcastPersistentManager) super.getManager(); + } + + public Session load(String id) throws ClassNotFoundException, IOException { + HazelcastSession hzSession = getMap().get(getManager().getName() + "_" + id); + if (null == hzSession) { + return null; + } + StandardSession session = null; + + byte[] data = (byte[]) hzSession.getData(); + if (data != null) { + ClassLoader appContextLoader = getManager().getContext().getLoader().getClassLoader(); + try (ObjectInputStream ois = Utils.getObjectInputStream(appContextLoader, + manager.getContext().getServletContext(), data)) { + + session = (StandardSession) this.manager.createEmptySession(); + session.readObjectData(ois); + + } + log.info("loaded" + id + " (" + session + ")"); + } + return session; + } + + public void remove(String id) throws IOException { + HazelcastSession removed = getMap().remove(id); + log.info("removed" + id + " (" + removed + ")"); + } + + public String[] keys() throws IOException { + return getMap().keySet().toArray(new String[0]); + } + + public int getSize() throws IOException { + return getMap().size(); + } + + public void clear() throws IOException { + getMap().clear(); + } + + private ReplicatedMap getMap() { + return instance.getReplicatedMap(mapName); + } + + // getters and setters + public void setMapName(String mapName) { + this.mapName = mapName; + } + + public void setMode(String mode) { + this.mode = mode; + } + + public void setGroup(String group) { + this.group = group; + } + + public void setAddresses(String addresses) { + this.addresses = addresses; + } + + public void setPort(int port) { + this.port = port; + } + + public void setMulticastGroup(String multicastGroup) { + this.multicastGroup = multicastGroup; + } + + public void setMulticastPort(int multicastPort) { + this.multicastPort = multicastPort; + } + + public void setMulticastTimeoutSeconds(int multicastTimeoutSeconds) { + this.multicastTimeoutSeconds = multicastTimeoutSeconds; + } + + public void setMulticastTtl(int multicastTtl) { + this.multicastTtl = multicastTtl; + } + + public void setTcpMembers(String tcpMembers) { + this.tcpMembers = tcpMembers; + } + +}