From b00e473fdc52c10c443c1c4733d5aa54b37f415c Mon Sep 17 00:00:00 2001 From: Frantisek Hartman Date: Tue, 26 Nov 2024 10:23:13 +0100 Subject: [PATCH] Move Spring Boot Session replication with WebFilter back [DEX-297] Some code samples were previously moved out into separate repositories. This makes them hard to maintain. Changes: - updates to Hazelcast 5.5.0 and Spring Boot 3.4.0 - added hazelcast-wm (previously this sample was using hazelcast-all that is no longer available) - fixed how thymeleaf access to session id - improved test TODO This sample uses hazelcast-wm library. Spring uses the jakarta namespace in the latest version, we updated to jakarta namespace in 5.1-SNAPSHOT. We need - update the hazelcast-wm version to 6.0 (updating to jakarta namespace is major breaking change) - do a release --- spring/pom.xml | 1 + .../README.adoc | 1 + .../pom.xml | 57 ++++++++++++++ .../springboot/http/Application.java | 32 ++++++++ .../springboot/http/WebController.java | 28 +++++++ .../src/main/resources/templates/index.html | 9 +++ .../springboot/http/ApplicationTest.java | 74 +++++++++++++++++++ 7 files changed, 202 insertions(+) create mode 100644 spring/spring-boot-session-replication-webfilter/README.adoc create mode 100644 spring/spring-boot-session-replication-webfilter/pom.xml create mode 100644 spring/spring-boot-session-replication-webfilter/src/main/java/com/hazelcast/springboot/http/Application.java create mode 100644 spring/spring-boot-session-replication-webfilter/src/main/java/com/hazelcast/springboot/http/WebController.java create mode 100644 spring/spring-boot-session-replication-webfilter/src/main/resources/templates/index.html create mode 100644 spring/spring-boot-session-replication-webfilter/src/test/java/com/hazelcast/springboot/http/ApplicationTest.java diff --git a/spring/pom.xml b/spring/pom.xml index 96f2b5d66..cbbfc0d17 100644 --- a/spring/pom.xml +++ b/spring/pom.xml @@ -37,6 +37,7 @@ + spring-boot-session-replication-webfilter spring-configuration spring-data-hazelcast-chemistry-sample spring-data-jpa-hazelcast-migration diff --git a/spring/spring-boot-session-replication-webfilter/README.adoc b/spring/spring-boot-session-replication-webfilter/README.adoc new file mode 100644 index 000000000..5da40dae3 --- /dev/null +++ b/spring/spring-boot-session-replication-webfilter/README.adoc @@ -0,0 +1 @@ +See the link:https://docs.hazelcast.com/tutorials/springboot-webfilter-session-replication[tutorial]. diff --git a/spring/spring-boot-session-replication-webfilter/pom.xml b/spring/spring-boot-session-replication-webfilter/pom.xml new file mode 100644 index 000000000..76388d7fe --- /dev/null +++ b/spring/spring-boot-session-replication-webfilter/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.0 + + + + com.hazelcast.samples + spring-boot-session-replication-webfilter + Spring Boot: Hazelcast embedded for Session Replication using WebFilter + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + com.hazelcast + hazelcast + 5.5.0 + + + com.hazelcast + hazelcast-wm + 5.1-SNAPSHOT + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.assertj + assertj-core + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/spring/spring-boot-session-replication-webfilter/src/main/java/com/hazelcast/springboot/http/Application.java b/spring/spring-boot-session-replication-webfilter/src/main/java/com/hazelcast/springboot/http/Application.java new file mode 100644 index 000000000..f8cb68c84 --- /dev/null +++ b/spring/spring-boot-session-replication-webfilter/src/main/java/com/hazelcast/springboot/http/Application.java @@ -0,0 +1,32 @@ +package com.hazelcast.springboot.http; + +import com.hazelcast.config.Config; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.web.WebFilter; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +import java.util.Properties; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + @Bean + public Config config() { + return new Config(); + } + + @Bean + public WebFilter webFilter(HazelcastInstance hazelcastInstance) { + Properties properties = new Properties(); + properties.put("instance-name", hazelcastInstance.getName()); + properties.put("sticky-session", "false"); + + return new WebFilter(properties); + } +} diff --git a/spring/spring-boot-session-replication-webfilter/src/main/java/com/hazelcast/springboot/http/WebController.java b/spring/spring-boot-session-replication-webfilter/src/main/java/com/hazelcast/springboot/http/WebController.java new file mode 100644 index 000000000..655f3859d --- /dev/null +++ b/spring/spring-boot-session-replication-webfilter/src/main/java/com/hazelcast/springboot/http/WebController.java @@ -0,0 +1,28 @@ +package com.hazelcast.springboot.http; + +import jakarta.servlet.http.HttpSession; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/") +public class WebController { + + @ModelAttribute("sessionId") + public String sessionId(final HttpSession session) { + return session.getId(); + } + + @RequestMapping(value = "/") + public String index(HttpSession httpSession) { + Integer hits = (Integer) httpSession.getAttribute("hits"); + if (hits == null) { + hits = 0; + } + httpSession.setAttribute("hits", ++hits); + + return "index"; + } + +} diff --git a/spring/spring-boot-session-replication-webfilter/src/main/resources/templates/index.html b/spring/spring-boot-session-replication-webfilter/src/main/resources/templates/index.html new file mode 100644 index 000000000..5a86eb206 --- /dev/null +++ b/spring/spring-boot-session-replication-webfilter/src/main/resources/templates/index.html @@ -0,0 +1,9 @@ + + + +

Session Id

+

+

Hits

+

+ + diff --git a/spring/spring-boot-session-replication-webfilter/src/test/java/com/hazelcast/springboot/http/ApplicationTest.java b/spring/spring-boot-session-replication-webfilter/src/test/java/com/hazelcast/springboot/http/ApplicationTest.java new file mode 100644 index 000000000..94771d462 --- /dev/null +++ b/spring/spring-boot-session-replication-webfilter/src/test/java/com/hazelcast/springboot/http/ApplicationTest.java @@ -0,0 +1,74 @@ +package com.hazelcast.springboot.http; + +import com.hazelcast.config.Config; +import org.junit.jupiter.api.Test; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ApplicationTest { + + @Test + public void testSessionReplication() { + // given + String port1 = startApplication(); + String port2 = startApplication(); + + // when + ResponseEntity response1 = makeRequest(port1); + String sessionId = extractCookie(response1, "JSESSIONID"); + String hazelcastSessionId = extractCookie(response1, "hazelcast.sessionId"); + ResponseEntity response2 = makeRequest(port2, sessionId, hazelcastSessionId); + + // then + String body = response2.getBody().toString(); + assertThat(body).contains(hazelcastSessionId); + assertThat(body).contains("

2

"); + } + + private static String startApplication() { + return new SpringApplicationBuilder(TestApplication.class) + .properties("server.port=0", "spring.main.allow-bean-definition-overriding=true") + .run() + .getEnvironment() + .getProperty("local.server.port"); + } + + private String extractCookie(ResponseEntity response, String cookie) { + return response.getHeaders().get("Set-Cookie").stream() + .filter(s -> s.contains(cookie)) + .map(s -> s.split(";")[0]) + .map(s -> s.substring(cookie.length() + 1)) + .findFirst().orElse(null); + } + + private static ResponseEntity makeRequest(String port) { + return makeRequest(port, null, null); + } + + private static ResponseEntity makeRequest(String port, String sessionId, String hazelcastSessionId) { + RestTemplate restTemplate = new RestTemplate(); + HttpHeaders headers = new HttpHeaders(); + if (sessionId != null || hazelcastSessionId != null) { + headers.add("Cookie", String.format("JSESSIONID=%s;hazelcast.sessionId=%s", sessionId, hazelcastSessionId)); + } + return restTemplate.exchange("http://localhost:" + port, HttpMethod.GET, new HttpEntity<>(headers), String.class); + } + + public static class TestApplication extends Application { + + @Bean + public Config config() { + Config config = super.config(); // Keep the test sync with the published content. + config.getNetworkConfig().getJoin().getMulticastConfig().setEnabled(true); + return config; + } + + } +}