feat: Add Thread-Specific Storage design pattern (#3422)

* Add Thread-Specific Storage design pattern

* fixed:pom.xml
Run 'mvn spotless:apply' to fix these violations.

* fix: address Sonar security hotspots for Random usage and e.printStackTrace()

* fixed:
Run 'mvn spotless:apply'(remove unused import)

* fixed:
Refactor Thread-Specific Storage pattern implementation to address Sonar issues: remove unnecessary instantiation, add private constructor, use diamond operator, and update tests.

* fixed:
update uml and README.md

* Trigger CI rerun
This commit is contained in:
CMD137 authored and GitHub committed 2026-06-01 21:11:47 +03:00
1 parent 48ba55a8a6
commit e8595393d6
13 files changed
+702

No files matched your search

@@ -0,0 +1,42 @@
package com.iluwatar.threadspecificstorage;
import lombok.extern.slf4j.Slf4j;
/**
* Application entry point demonstrating the Thread-Specific Storage pattern.
*
* <p>This example simulates concurrent request processing for multiple users. Each request carries
* a user token, and user-specific context is managed transparently using thread-specific storage.
*/
@Slf4j
public class App {
/**
* Runs the Thread-Specific Storage pattern demonstration.
*
* @param args command-line arguments (not used)
*/
public static void main(String[] args) {
// Simulate concurrent requests from multiple users
for (int i = 1; i <= 5; i++) {
// Simulate tokens for different users
String token = "token::" + (i % 3 + 1); // 3 distinct users
new Thread(
() -> {
// Simulate request processing flow
RequestHandler handler = new RequestHandler(token);
handler.process();
})
.start();
// Slightly stagger request times
try {
Thread.sleep(50);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOGGER.warn("Sleep interrupted", e);
}
}
}
}
@@ -0,0 +1,57 @@
package com.iluwatar.threadspecificstorage;
import java.security.SecureRandom;
import lombok.extern.slf4j.Slf4j;
/**
* Application Thread
*
* <p>Each instance simulates a request-processing thread that uses the Thread-Specific Object Proxy
* to access Thread-Specific Object.
*/
@Slf4j
public class RequestHandler {
private final String token;
public RequestHandler(String token) {
this.token = token;
}
/**
* Simulated business process: 1. Parse userId from token ("Token::userId"). 2. Store userId in
* thread-local storage. 3. Later, retrieve userId and use it for business logic. 4. Finally,
* clear thread-local to prevent memory leak.
*/
public void process() {
LOGGER.info("Start handling request with token: {}", token);
try {
// Step 1: Parse token to get userId
Long userId = parseToken(token);
// Step 2: Save userId in ThreadLocal storage
UserContextProxy.set(new UserContext(userId));
// Simulate delay between stages of request handling
Thread.sleep(200);
// Step 3: Retrieve userId later in the request flow
Long retrievedId = UserContextProxy.get().getUserId();
SecureRandom random = new SecureRandom();
String accountInfo = retrievedId + "'s account: " + random.nextInt(400);
LOGGER.info(accountInfo);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
// Step 4: Clear ThreadLocal to avoid potential memory leaks
UserContextProxy.clear();
}
}
private Long parseToken(String token) {
// token format: "Token::1234"
String[] parts = token.split("::");
return (parts.length == 2) ? Long.parseLong(parts[1]) : -1L;
}
}
@@ -0,0 +1,15 @@
package com.iluwatar.threadspecificstorage;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* Thread-Specific Object
*
* <p>Provides a service or data that is only accessible via a particular thread.
*/
@Data
@AllArgsConstructor
public class UserContext {
private Long userId;
}
@@ -0,0 +1,33 @@
package com.iluwatar.threadspecificstorage;
/**
* Thread-Specific Object Proxy
*
* <p>The Thread-Specific Object Proxy acts as an intermediary, enabling application thread to
* access and manipulate thread-specific objects simply and securely.
*/
public class UserContextProxy {
/**
* Underlying TSObjectCollection (ThreadLocalMap) managed by JVM.This ThreadLocal acts as the Key
* for the map.So That there is also no key factory.
*/
private static final ThreadLocal<UserContext> userContextHolder = new ThreadLocal<>();
/** Private constructor to prevent instantiation of this utility class. */
private UserContextProxy() {}
/** Set UserContext for the current thread. */
public static void set(UserContext context) {
userContextHolder.set(context);
}
/** Get UserContext for the current thread. */
public static UserContext get() {
return userContextHolder.get();
}
/** Clear UserContext to prevent potential memory leaks. */
public static void clear() {
userContextHolder.remove();
}
}
@@ -0,0 +1,38 @@
package com.iluwatar.threadspecificstorage;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import org.junit.jupiter.api.Test;
/** Tests for APP class */
class AppTest {
@Test
void testMainMethod() {
// Capture system output
ByteArrayOutputStream outContent = new ByteArrayOutputStream();
System.setOut(new PrintStream(outContent));
// Run the main method
App.main(new String[] {});
// Give some time for threads to execute
await()
.atMost(5, SECONDS)
.pollInterval(100, MILLISECONDS)
.until(() -> outContent.toString().contains("Start handling request with token"));
// Verify output contains expected log messages
String output = outContent.toString();
assertTrue(
output.contains("Start handling request with token"),
"Should contain request handling start messages");
// Restore system output
System.setOut(System.out);
}
}
@@ -0,0 +1,32 @@
package com.iluwatar.threadspecificstorage;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
class RequestHandlerTest {
@Test
void process_shouldStoreAndClearUserContext() {
// Given - a request handler without proxy parameter
RequestHandler handler = new RequestHandler("token::123");
// When - process the request
handler.process();
// Then - after processing, ThreadLocal should be cleared
assertNull(UserContextProxy.get(), "ThreadLocal should be cleared after process()");
}
@Test
void process_withInvalidToken_shouldSetUserIdToMinusOne() {
// Given - a request handler without proxy parameter
RequestHandler handler = new RequestHandler("invalid-token");
// When - process the request
handler.process();
// Then - after processing, ThreadLocal should be cleared
assertNull(UserContextProxy.get(), "ThreadLocal should be cleared even for invalid token");
}
}
@@ -0,0 +1,70 @@
package com.iluwatar.threadspecificstorage;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Tests for UserContextProxy class */
class UserContextProxyTest {
private UserContext userContext;
@BeforeEach
void setUp() {
userContext = new UserContext(123L);
}
@AfterEach
void tearDown() {
UserContextProxy.clear();
}
@Test
void testSetAndGetContext() {
UserContextProxy.set(userContext);
UserContext retrievedContext = UserContextProxy.get();
assertNotNull(retrievedContext, "Retrieved context should not be null");
assertEquals(
userContext.getUserId(),
retrievedContext.getUserId(),
"Retrieved context should have the same userId");
}
@Test
void testGetContextWhenNotSet() {
UserContext retrievedContext = UserContextProxy.get();
assertNull(retrievedContext, "Context should be null when not set");
}
@Test
void testClearContext() {
UserContextProxy.set(userContext);
UserContextProxy.clear();
UserContext retrievedContext = UserContextProxy.get();
assertNull(retrievedContext, "Context should be null after clearing");
}
@Test
void testThreadIsolation() throws InterruptedException {
UserContext context1 = new UserContext(123L);
UserContext context2 = new UserContext(456L);
UserContextProxy.set(context1);
// Create another thread to set different context
Thread thread =
new Thread(
() -> {
UserContextProxy.set(context2);
UserContext threadContext = UserContextProxy.get();
assertNotNull(threadContext);
assertEquals(456L, threadContext.getUserId());
});
thread.start();
thread.join();
// Main thread context should remain unchanged
UserContext mainThreadContext = UserContextProxy.get();
assertNotNull(mainThreadContext);
assertEquals(123L, mainThreadContext.getUserId());
}
}
@@ -0,0 +1,57 @@
package com.iluwatar.threadspecificstorage;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
/** Tests for UserContext class */
class UserContextTest {
@Test
void testConstructorAndGetUserId() {
Long userId = 123L;
UserContext context = new UserContext(userId);
assertEquals(
userId, context.getUserId(), "UserId should match the one provided in constructor");
}
@Test
void testSetUserId() {
UserContext context = new UserContext(123L);
Long newUserId = 456L;
context.setUserId(newUserId);
assertEquals(newUserId, context.getUserId(), "UserId should be updated");
}
@Test
void testToString() {
Long userId = 123L;
UserContext context = new UserContext(userId);
String expected = "UserContext(userId=" + userId + ")";
assertEquals(expected, context.toString(), "toString should return expected format");
}
@Test
void testEqualsAndHashCode() {
Long userId = 123L;
UserContext context1 = new UserContext(userId);
UserContext context2 = new UserContext(userId);
UserContext context3 = new UserContext(456L);
assertEquals(context1, context2, "Objects with same userId should be equal");
assertEquals(
context1.hashCode(),
context2.hashCode(),
"Objects with same userId should have same hashCode");
assertNotEquals(context1, context3, "Objects with different userId should not be equal");
assertNotEquals(
context1.hashCode(),
context3.hashCode(),
"Objects with different userId should have different hashCode");
}
}