Skip to content

Commit cc85e92

Browse files
authored
Add optional Hazelcast session serializer
Issue gh-1131
1 parent 0819988 commit cc85e92

File tree

5 files changed

+279
-2
lines changed

5 files changed

+279
-2
lines changed

spring-session-docs/src/docs/asciidoc/guides/java-hazelcast.adoc

+8-1
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,18 @@ The filter is in charge of replacing the `HttpSession` implementation to be back
9797
In this instance, Spring Session is backed by Hazelcast.
9898
<2> In order to support retrieval of sessions by principal name index, an appropriate `ValueExtractor` needs to be registered.
9999
Spring Session provides `PrincipalNameExtractor` for this purpose.
100-
<3> We create a `HazelcastInstance` that connects Spring Session to Hazelcast.
100+
<3> In order to serialize `MapSession` objects efficiently, `HazelcastSessionSerializer` needs to be registered. If this
101+
is not set, Hazelcast will serialize sessions using native Java serialization.
102+
<4> We create a `HazelcastInstance` that connects Spring Session to Hazelcast.
101103
By default, the application starts and connects to an embedded instance of Hazelcast.
102104
For more information on configuring Hazelcast, see the https://docs.hazelcast.org/docs/{hazelcast-version}/manual/html-single/index.html#hazelcast-configuration[reference documentation].
103105
====
104106

107+
NOTE: If `HazelcastSessionSerializer` is preferred, it needs to be configured for all Hazelcast cluster members before they start.
108+
In a Hazelcast cluster, all members should use the same serialization method for sessions. Also, if Hazelcast Client/Server topology
109+
is used, then both members and clients must use the same serialization method. The serializer can be registered via `ClientConfig`
110+
with the same `SerializerConfiguration` of members.
111+
105112
== Servlet Container Initialization
106113

107114
Our <<security-spring-configuration,Spring Configuration>> created a Spring bean named `springSessionRepositoryFilter` that implements `Filter`.

spring-session-docs/src/test/java/docs/http/HazelcastHttpSessionConfig.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,15 @@
1919
import com.hazelcast.config.Config;
2020
import com.hazelcast.config.MapAttributeConfig;
2121
import com.hazelcast.config.MapIndexConfig;
22+
import com.hazelcast.config.SerializerConfig;
2223
import com.hazelcast.core.Hazelcast;
2324
import com.hazelcast.core.HazelcastInstance;
2425

2526
import org.springframework.context.annotation.Bean;
2627
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.session.MapSession;
2729
import org.springframework.session.hazelcast.HazelcastIndexedSessionRepository;
30+
import org.springframework.session.hazelcast.HazelcastSessionSerializer;
2831
import org.springframework.session.hazelcast.PrincipalNameExtractor;
2932
import org.springframework.session.hazelcast.config.annotation.web.http.EnableHazelcastHttpSession;
3033

@@ -42,7 +45,10 @@ public HazelcastInstance hazelcastInstance() {
4245
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME) // <2>
4346
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
4447
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
45-
return Hazelcast.newHazelcastInstance(config); // <3>
48+
SerializerConfig serializerConfig = new SerializerConfig();
49+
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
50+
config.getSerializationConfig().addSerializerConfig(serializerConfig); // <3>
51+
return Hazelcast.newHazelcastInstance(config); // <4>
4652
}
4753

4854
}

spring-session-hazelcast/src/integration-test/java/org/springframework/session/hazelcast/HazelcastITestUtils.java

+6
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,12 @@
2020
import com.hazelcast.config.MapAttributeConfig;
2121
import com.hazelcast.config.MapIndexConfig;
2222
import com.hazelcast.config.NetworkConfig;
23+
import com.hazelcast.config.SerializerConfig;
2324
import com.hazelcast.core.Hazelcast;
2425
import com.hazelcast.core.HazelcastInstance;
2526

27+
import org.springframework.session.MapSession;
28+
2629
/**
2730
* Utility class for Hazelcast integration tests.
2831
*
@@ -48,6 +51,9 @@ static HazelcastInstance embeddedHazelcastServer() {
4851
config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME)
4952
.addMapAttributeConfig(attributeConfig).addMapIndexConfig(
5053
new MapIndexConfig(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE, false));
54+
SerializerConfig serializerConfig = new SerializerConfig();
55+
serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
56+
config.getSerializationConfig().addSerializerConfig(serializerConfig);
5157
return Hazelcast.newHazelcastInstance(config);
5258
}
5359

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
* Copyright 2014-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.session.hazelcast;
18+
19+
import java.io.EOFException;
20+
import java.io.IOException;
21+
import java.time.Duration;
22+
import java.time.Instant;
23+
24+
import com.hazelcast.nio.ObjectDataInput;
25+
import com.hazelcast.nio.ObjectDataOutput;
26+
import com.hazelcast.nio.serialization.StreamSerializer;
27+
28+
import org.springframework.session.MapSession;
29+
30+
/**
31+
* A {@link com.hazelcast.nio.serialization.Serializer} implementation that handles the
32+
* (de)serialization of {@link MapSession} stored on {@link com.hazelcast.core.IMap}.
33+
*
34+
* <p>
35+
* The use of this serializer is optional and provides faster serialization of sessions.
36+
* If not configured to be used, Hazelcast will serialize sessions via
37+
* {@link java.io.Serializable} by default.
38+
*
39+
* <p>
40+
* If multiple instances of a Spring application is run, then all of them need to use the
41+
* same serialization method. If this serializer is registered on one instance and not
42+
* another one, then it will end up with HazelcastSerializationException. The same applies
43+
* when clients are configured to use this serializer but not the members, and vice versa.
44+
* Also note that, if a new instance is created with this serialization but the existing
45+
* Hazelcast cluster contains the values not serialized by this but instead the default
46+
* one, this will result in incompatibility again.
47+
*
48+
* <p>
49+
* An example of how to register the serializer on embedded instance can be seen below:
50+
*
51+
* <pre class="code">
52+
* Config config = new Config();
53+
*
54+
* // ... other configurations for Hazelcast ...
55+
*
56+
* SerializerConfig serializerConfig = new SerializerConfig();
57+
* serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
58+
* config.getSerializationConfig().addSerializerConfig(serializerConfig);
59+
*
60+
* HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);
61+
* </pre>
62+
*
63+
* Below is the example of how to register the serializer on client instance. Note that,
64+
* to use the serializer in client/server mode, the serializer - and hence
65+
* {@link MapSession}, must exist on the server's classpath and must be registered via
66+
* {@link com.hazelcast.config.SerializerConfig} with the configuration above for each
67+
* server.
68+
*
69+
* <pre class="code">
70+
* ClientConfig clientConfig = new ClientConfig();
71+
*
72+
* // ... other configurations for Hazelcast Client ...
73+
*
74+
* SerializerConfig serializerConfig = new SerializerConfig();
75+
* serializerConfig.setImplementation(new HazelcastSessionSerializer()).setTypeClass(MapSession.class);
76+
* clientConfig.getSerializationConfig().addSerializerConfig(serializerConfig);
77+
*
78+
* HazelcastInstance hazelcastClient = HazelcastClient.newHazelcastClient(clientConfig);
79+
* </pre>
80+
*
81+
* @author Enes Ozcan
82+
* @since 2.4.0
83+
*/
84+
public class HazelcastSessionSerializer implements StreamSerializer<MapSession> {
85+
86+
private static final int SERIALIZER_TYPE_ID = 1453;
87+
88+
@Override
89+
public void write(ObjectDataOutput out, MapSession session) throws IOException {
90+
out.writeUTF(session.getOriginalId());
91+
out.writeUTF(session.getId());
92+
writeInstant(out, session.getCreationTime());
93+
writeInstant(out, session.getLastAccessedTime());
94+
writeDuration(out, session.getMaxInactiveInterval());
95+
for (String attrName : session.getAttributeNames()) {
96+
Object attrValue = session.getAttribute(attrName);
97+
if (attrValue != null) {
98+
out.writeUTF(attrName);
99+
out.writeObject(attrValue);
100+
}
101+
}
102+
}
103+
104+
private void writeInstant(ObjectDataOutput out, Instant instant) throws IOException {
105+
out.writeLong(instant.getEpochSecond());
106+
out.writeInt(instant.getNano());
107+
}
108+
109+
private void writeDuration(ObjectDataOutput out, Duration duration) throws IOException {
110+
out.writeLong(duration.getSeconds());
111+
out.writeInt(duration.getNano());
112+
}
113+
114+
@Override
115+
public MapSession read(ObjectDataInput in) throws IOException {
116+
String originalId = in.readUTF();
117+
MapSession cached = new MapSession(originalId);
118+
cached.setId(in.readUTF());
119+
cached.setCreationTime(readInstant(in));
120+
cached.setLastAccessedTime(readInstant(in));
121+
cached.setMaxInactiveInterval(readDuration(in));
122+
try {
123+
while (true) {
124+
// During write, it's not possible to write
125+
// number of non-null attributes without an extra
126+
// iteration. Hence the attributes are read until
127+
// EOF here.
128+
String attrName = in.readUTF();
129+
Object attrValue = in.readObject();
130+
cached.setAttribute(attrName, attrValue);
131+
}
132+
}
133+
catch (EOFException ignored) {
134+
}
135+
return cached;
136+
}
137+
138+
private Instant readInstant(ObjectDataInput in) throws IOException {
139+
long seconds = in.readLong();
140+
int nanos = in.readInt();
141+
return Instant.ofEpochSecond(seconds, nanos);
142+
}
143+
144+
private Duration readDuration(ObjectDataInput in) throws IOException {
145+
long seconds = in.readLong();
146+
int nanos = in.readInt();
147+
return Duration.ofSeconds(seconds, nanos);
148+
}
149+
150+
@Override
151+
public int getTypeId() {
152+
return SERIALIZER_TYPE_ID;
153+
}
154+
155+
@Override
156+
public void destroy() {
157+
}
158+
159+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright 2014-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.session.hazelcast;
18+
19+
import java.io.Serializable;
20+
import java.time.Duration;
21+
import java.time.Instant;
22+
23+
import com.hazelcast.config.SerializationConfig;
24+
import com.hazelcast.config.SerializerConfig;
25+
import com.hazelcast.internal.serialization.impl.DefaultSerializationServiceBuilder;
26+
import com.hazelcast.nio.serialization.Data;
27+
import com.hazelcast.spi.serialization.SerializationService;
28+
import org.junit.jupiter.api.BeforeEach;
29+
import org.junit.jupiter.api.Test;
30+
31+
import org.springframework.session.MapSession;
32+
33+
import static org.assertj.core.api.Assertions.assertThat;
34+
35+
class HazelcastSessionSerializerTests {
36+
37+
private SerializationService serializationService;
38+
39+
@BeforeEach
40+
void setUp() {
41+
SerializationConfig serializationConfig = new SerializationConfig();
42+
SerializerConfig serializerConfig = new SerializerConfig().setImplementation(new HazelcastSessionSerializer())
43+
.setTypeClass(MapSession.class);
44+
serializationConfig.addSerializerConfig(serializerConfig);
45+
this.serializationService = new DefaultSerializationServiceBuilder().setConfig(serializationConfig).build();
46+
}
47+
48+
@Test
49+
void serializeSessionWithStreamSerializer() {
50+
MapSession originalSession = new MapSession();
51+
originalSession.setAttribute("attr1", "value1");
52+
originalSession.setAttribute("attr2", "value2");
53+
originalSession.setAttribute("attr3", new SerializableTestAttribute(3));
54+
originalSession.setMaxInactiveInterval(Duration.ofDays(5));
55+
originalSession.setLastAccessedTime(Instant.now());
56+
originalSession.setId("custom-id");
57+
58+
Data serialized = this.serializationService.toData(originalSession);
59+
MapSession cached = this.serializationService.toObject(serialized);
60+
61+
assertThat(originalSession.getCreationTime()).isEqualTo(cached.getCreationTime());
62+
assertThat(originalSession.getMaxInactiveInterval()).isEqualTo(cached.getMaxInactiveInterval());
63+
assertThat(originalSession.getId()).isEqualTo(cached.getId());
64+
assertThat(originalSession.getOriginalId()).isEqualTo(cached.getOriginalId());
65+
assertThat(originalSession.getAttributeNames().size()).isEqualTo(cached.getAttributeNames().size());
66+
assertThat(originalSession.<String>getAttribute("attr1")).isEqualTo(cached.getAttribute("attr1"));
67+
assertThat(originalSession.<String>getAttribute("attr2")).isEqualTo(cached.getAttribute("attr2"));
68+
assertThat(originalSession.<SerializableTestAttribute>getAttribute("attr3"))
69+
.isEqualTo(cached.getAttribute("attr3"));
70+
}
71+
72+
static class SerializableTestAttribute implements Serializable {
73+
74+
private int id;
75+
76+
SerializableTestAttribute(int id) {
77+
this.id = id;
78+
}
79+
80+
@Override
81+
public boolean equals(Object o) {
82+
if (this == o) {
83+
return true;
84+
}
85+
if (!(o instanceof SerializableTestAttribute)) {
86+
return false;
87+
}
88+
SerializableTestAttribute that = (SerializableTestAttribute) o;
89+
return this.id == that.id;
90+
}
91+
92+
@Override
93+
public int hashCode() {
94+
return this.id;
95+
}
96+
97+
}
98+
99+
}

0 commit comments

Comments
 (0)