Add optional Hazelcast session serializer
Issue gh-1131
This commit is contained in:
@@ -97,11 +97,18 @@ The filter is in charge of replacing the `HttpSession` implementation to be back
|
|||||||
In this instance, Spring Session is backed by Hazelcast.
|
In this instance, Spring Session is backed by Hazelcast.
|
||||||
<2> In order to support retrieval of sessions by principal name index, an appropriate `ValueExtractor` needs to be registered.
|
<2> In order to support retrieval of sessions by principal name index, an appropriate `ValueExtractor` needs to be registered.
|
||||||
Spring Session provides `PrincipalNameExtractor` for this purpose.
|
Spring Session provides `PrincipalNameExtractor` for this purpose.
|
||||||
<3> We create a `HazelcastInstance` that connects Spring Session to Hazelcast.
|
<3> In order to serialize `MapSession` objects efficiently, `HazelcastSessionSerializer` needs to be registered. If this
|
||||||
|
is not set, Hazelcast will serialize sessions using native Java serialization.
|
||||||
|
<4> We create a `HazelcastInstance` that connects Spring Session to Hazelcast.
|
||||||
By default, the application starts and connects to an embedded instance of Hazelcast.
|
By default, the application starts and connects to an embedded instance of Hazelcast.
|
||||||
For more information on configuring Hazelcast, see the https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#hazelcast-configuration[reference documentation].
|
For more information on configuring Hazelcast, see the https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#hazelcast-configuration[reference documentation].
|
||||||
====
|
====
|
||||||
|
|
||||||
|
NOTE: If `HazelcastSessionSerializer` is preferred, it needs to be configured for all Hazelcast cluster members before they start.
|
||||||
|
In a Hazelcast cluster, all members should use the same serialization method for sessions. Also, if Hazelcast Client/Server topology
|
||||||
|
is used, then both members and clients must use the same serialization method. The serializer can be registered via `ClientConfig`
|
||||||
|
with the same `SerializerConfiguration` of members.
|
||||||
|
|
||||||
== Servlet Container Initialization
|
== Servlet Container Initialization
|
||||||
|
|
||||||
Our <<security-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
Our <<security-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.
|
||||||
|
|||||||
@@ -19,12 +19,15 @@ package docs.http;
|
|||||||
import com.hazelcast.config.Config;
|
import com.hazelcast.config.Config;
|
||||||
import com.hazelcast.config.MapAttributeConfig;
|
import com.hazelcast.config.MapAttributeConfig;
|
||||||
import com.hazelcast.config.MapIndexConfig;
|
import com.hazelcast.config.MapIndexConfig;
|
||||||
|
import com.hazelcast.config.SerializerConfig;
|
||||||
import com.hazelcast.core.Hazelcast;
|
import com.hazelcast.core.Hazelcast;
|
||||||
import com.hazelcast.core.HazelcastInstance;
|
import com.hazelcast.core.HazelcastInstance;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.session.MapSession;
|
||||||
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
|
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
|
||||||
|
import org.springframework.session.hazelcast.HazelcastSessionSerializer;
|
||||||
import org.springframework.session.hazelcast.PrincipalNameExtractor;
|
import org.springframework.session.hazelcast.PrincipalNameExtractor;
|
||||||
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
|
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
|
||||||
|
|
||||||
@@ -42,7 +45,10 @@ public class HazelcastHttpSessionConfig {
|
|||||||
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
|
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
|
||||||
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
|
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
|
||||||
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
|
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
|
||||||
return Hazelcast.newHazelcastInstance(config); // <3>
|
SerializerConfig serializerConfig = new SerializerConfig();
|
||||||
|
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
|
||||||
|
config.getSerializationConfig().addSerializerConfig(serializerConfig); // <3>
|
||||||
|
return Hazelcast.newHazelcastInstance(config); // <4>
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,12 @@ import com.hazelcast.config.Config;
|
|||||||
import com.hazelcast.config.MapAttributeConfig;
|
import com.hazelcast.config.MapAttributeConfig;
|
||||||
import com.hazelcast.config.MapIndexConfig;
|
import com.hazelcast.config.MapIndexConfig;
|
||||||
import com.hazelcast.config.NetworkConfig;
|
import com.hazelcast.config.NetworkConfig;
|
||||||
|
import com.hazelcast.config.SerializerConfig;
|
||||||
import com.hazelcast.core.Hazelcast;
|
import com.hazelcast.core.Hazelcast;
|
||||||
import com.hazelcast.core.HazelcastInstance;
|
import com.hazelcast.core.HazelcastInstance;
|
||||||
|
|
||||||
|
import org.springframework.session.MapSession;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class for Hazelcast integration tests.
|
* Utility class for Hazelcast integration tests.
|
||||||
*
|
*
|
||||||
@@ -48,6 +51,9 @@ final class HazelcastITestUtils {
|
|||||||
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
|
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
|
||||||
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
|
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
|
||||||
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
|
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
|
||||||
|
SerializerConfig serializerConfig = new SerializerConfig();
|
||||||
|
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
|
||||||
|
config.getSerializationConfig().addSerializerConfig(serializerConfig);
|
||||||
return Hazelcast.newHazelcastInstance(config);
|
return Hazelcast.newHazelcastInstance(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014-2020 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
|
||||||
|
*
|
||||||
|
* https://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.springframework.session.hazelcast;
|
||||||
|
|
||||||
|
import java.io.EOFException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import com.hazelcast.nio.ObjectDataInput;
|
||||||
|
import com.hazelcast.nio.ObjectDataOutput;
|
||||||
|
import com.hazelcast.nio.serialization.StreamSerializer;
|
||||||
|
|
||||||
|
import org.springframework.session.MapSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link com.hazelcast.nio.serialization.Serializer} implementation that handles the
|
||||||
|
* (de)serialization of {@link MapSession} stored on {@link com.hazelcast.core.IMap}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The use of this serializer is optional and provides faster serialization of sessions.
|
||||||
|
* If not configured to be used, Hazelcast will serialize sessions via
|
||||||
|
* {@link java.io.Serializable} by default.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* If multiple instances of a Spring application is run, then all of them need to use the
|
||||||
|
* same serialization method. If this serializer is registered on one instance and not
|
||||||
|
* another one, then it will end up with HazelcastSerializationException. The same applies
|
||||||
|
* when clients are configured to use this serializer but not the members, and vice versa.
|
||||||
|
* Also note that, if a new instance is created with this serialization but the existing
|
||||||
|
* Hazelcast cluster contains the values not serialized by this but instead the default
|
||||||
|
* one, this will result in incompatibility again.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* An example of how to register the serializer on embedded instance can be seen below:
|
||||||
|
*
|
||||||
|
* <pre class="code">
|
||||||
|
* Config config = new Config();
|
||||||
|
*
|
||||||
|
* // ... other configurations for Hazelcast ...
|
||||||
|
*
|
||||||
|
* SerializerConfig serializerConfig = new SerializerConfig();
|
||||||
|
* serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
|
||||||
|
* config.getSerializationConfig().addSerializerConfig(serializerConfig);
|
||||||
|
*
|
||||||
|
* HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* Below is the example of how to register the serializer on client instance. Note that,
|
||||||
|
* to use the serializer in client/server mode, the serializer - and hence
|
||||||
|
* {@link MapSession}, must exist on the server's classpath and must be registered via
|
||||||
|
* {@link com.hazelcast.config.SerializerConfig} with the configuration above for each
|
||||||
|
* server.
|
||||||
|
*
|
||||||
|
* <pre class="code">
|
||||||
|
* ClientConfig clientConfig = new ClientConfig();
|
||||||
|
*
|
||||||
|
* // ... other configurations for Hazelcast Client ...
|
||||||
|
*
|
||||||
|
* SerializerConfig serializerConfig = new SerializerConfig();
|
||||||
|
* serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
|
||||||
|
* clientConfig.getSerializationConfig().addSerializerConfig(serializerConfig);
|
||||||
|
*
|
||||||
|
* HazelcastInstance hazelcastClient = HazelcastClient.newHazelcastClient(clientConfig);
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* @author Enes Ozcan
|
||||||
|
* @since 2.4.0
|
||||||
|
*/
|
||||||
|
public class HazelcastSessionSerializer implements StreamSerializer<MapSession> {
|
||||||
|
|
||||||
|
private static final int SERIALIZER_TYPE_ID = 1453;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(ObjectDataOutput out, MapSession session) throws IOException {
|
||||||
|
out.writeUTF(session.getOriginalId());
|
||||||
|
out.writeUTF(session.getId());
|
||||||
|
writeInstant(out, session.getCreationTime());
|
||||||
|
writeInstant(out, session.getLastAccessedTime());
|
||||||
|
writeDuration(out, session.getMaxInactiveInterval());
|
||||||
|
for (String attrName : session.getAttributeNames()) {
|
||||||
|
Object attrValue = session.getAttribute(attrName);
|
||||||
|
if (attrValue != null) {
|
||||||
|
out.writeUTF(attrName);
|
||||||
|
out.writeObject(attrValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeInstant(ObjectDataOutput out, Instant instant) throws IOException {
|
||||||
|
out.writeLong(instant.getEpochSecond());
|
||||||
|
out.writeInt(instant.getNano());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void writeDuration(ObjectDataOutput out, Duration duration) throws IOException {
|
||||||
|
out.writeLong(duration.getSeconds());
|
||||||
|
out.writeInt(duration.getNano());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MapSession read(ObjectDataInput in) throws IOException {
|
||||||
|
String originalId = in.readUTF();
|
||||||
|
MapSession cached = new MapSession(originalId);
|
||||||
|
cached.setId(in.readUTF());
|
||||||
|
cached.setCreationTime(readInstant(in));
|
||||||
|
cached.setLastAccessedTime(readInstant(in));
|
||||||
|
cached.setMaxInactiveInterval(readDuration(in));
|
||||||
|
try {
|
||||||
|
while (true) {
|
||||||
|
// During write, it's not possible to write
|
||||||
|
// number of non-null attributes without an extra
|
||||||
|
// iteration. Hence the attributes are read until
|
||||||
|
// EOF here.
|
||||||
|
String attrName = in.readUTF();
|
||||||
|
Object attrValue = in.readObject();
|
||||||
|
cached.setAttribute(attrName, attrValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (EOFException ignored) {
|
||||||
|
}
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Instant readInstant(ObjectDataInput in) throws IOException {
|
||||||
|
long seconds = in.readLong();
|
||||||
|
int nanos = in.readInt();
|
||||||
|
return Instant.ofEpochSecond(seconds, nanos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Duration readDuration(ObjectDataInput in) throws IOException {
|
||||||
|
long seconds = in.readLong();
|
||||||
|
int nanos = in.readInt();
|
||||||
|
return Duration.ofSeconds(seconds, nanos);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getTypeId() {
|
||||||
|
return SERIALIZER_TYPE_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2014-2020 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
|
||||||
|
*
|
||||||
|
* https://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.springframework.session.hazelcast;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import com.hazelcast.config.SerializationConfig;
|
||||||
|
import com.hazelcast.config.SerializerConfig;
|
||||||
|
import com.hazelcast.internal.serialization.impl.DefaultSerializationServiceBuilder;
|
||||||
|
import com.hazelcast.nio.serialization.Data;
|
||||||
|
import com.hazelcast.spi.serialization.SerializationService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.session.MapSession;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class HazelcastSessionSerializerTests {
|
||||||
|
|
||||||
|
private SerializationService serializationService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
SerializationConfig serializationConfig = new SerializationConfig();
|
||||||
|
SerializerConfig serializerConfig = new SerializerConfig().setImplementation(new HazelcastSessionSerializer())
|
||||||
|
.setTypeClass(MapSession.class);
|
||||||
|
serializationConfig.addSerializerConfig(serializerConfig);
|
||||||
|
this.serializationService = new DefaultSerializationServiceBuilder().setConfig(serializationConfig).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void serializeSessionWithStreamSerializer() {
|
||||||
|
MapSession originalSession = new MapSession();
|
||||||
|
originalSession.setAttribute("attr1", "value1");
|
||||||
|
originalSession.setAttribute("attr2", "value2");
|
||||||
|
originalSession.setAttribute("attr3", new SerializableTestAttribute(3));
|
||||||
|
originalSession.setMaxInactiveInterval(Duration.ofDays(5));
|
||||||
|
originalSession.setLastAccessedTime(Instant.now());
|
||||||
|
originalSession.setId("custom-id");
|
||||||
|
|
||||||
|
Data serialized = this.serializationService.toData(originalSession);
|
||||||
|
MapSession cached = this.serializationService.toObject(serialized);
|
||||||
|
|
||||||
|
assertThat(originalSession.getCreationTime()).isEqualTo(cached.getCreationTime());
|
||||||
|
assertThat(originalSession.getMaxInactiveInterval()).isEqualTo(cached.getMaxInactiveInterval());
|
||||||
|
assertThat(originalSession.getId()).isEqualTo(cached.getId());
|
||||||
|
assertThat(originalSession.getOriginalId()).isEqualTo(cached.getOriginalId());
|
||||||
|
assertThat(originalSession.getAttributeNames().size()).isEqualTo(cached.getAttributeNames().size());
|
||||||
|
assertThat(originalSession.<String>getAttribute("attr1")).isEqualTo(cached.getAttribute("attr1"));
|
||||||
|
assertThat(originalSession.<String>getAttribute("attr2")).isEqualTo(cached.getAttribute("attr2"));
|
||||||
|
assertThat(originalSession.<SerializableTestAttribute>getAttribute("attr3"))
|
||||||
|
.isEqualTo(cached.getAttribute("attr3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static class SerializableTestAttribute implements Serializable {
|
||||||
|
|
||||||
|
private int id;
|
||||||
|
|
||||||
|
SerializableTestAttribute(int id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (!(o instanceof SerializableTestAttribute)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
SerializableTestAttribute that = (SerializableTestAttribute) o;
|
||||||
|
return this.id == that.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return this.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user