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,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");
}
}