mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-10-04 12:13:11 +00:00
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:
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+57
@@ -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;
|
||||
}
|
||||
}
|
||||
+15
@@ -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;
|
||||
}
|
||||
+33
@@ -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);
|
||||
}
|
||||
}
|
||||
+32
@@ -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");
|
||||
}
|
||||
}
|
||||
+70
@@ -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());
|
||||
}
|
||||
}
|
||||
+57
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user