mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-09-08 02:19:56 +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:
@@ -234,6 +234,7 @@
|
||||
<module>template-method</module>
|
||||
<module>templateview</module>
|
||||
<module>thread-pool-executor</module>
|
||||
<module>thread-specific-storage</module>
|
||||
<module>throttling</module>
|
||||
<module>tolerant-reader</module>
|
||||
<module>trampoline</module>
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
---
|
||||
title: "Thread-Specific Storage Pattern in Java: Isolated Thread-Local Data Management"
|
||||
shortTitle: Thread-Specific Storage
|
||||
description: "Learn the Thread-Specific Storage pattern in Java with practical examples, class
|
||||
diagrams, and implementation details. Understand how to manage thread-local data efficiently,
|
||||
improving concurrency and avoiding synchronization issues."
|
||||
category: Concurrency
|
||||
language: en
|
||||
tag:
|
||||
|
||||
- Concurrency
|
||||
- Multithreading
|
||||
- Thread Safety
|
||||
- Data Isolation
|
||||
- Memory Management
|
||||
|
||||
---
|
||||
|
||||
## Intent of Thread-Specific Storage Design Pattern
|
||||
|
||||
The Thread-Specific Storage pattern ensures that each thread has its own isolated instance of shared data,
|
||||
preventing concurrency issues by eliminating the need for synchronization. It achieves this by using
|
||||
ThreadLocal variables to store data that is specific to each thread.
|
||||
|
||||
## Detailed Explanation of Thread-Specific Storage Pattern with Real-World Examples
|
||||
|
||||
### Real-world example
|
||||
|
||||
> Think of a customer service center where each agent has their own notepad to record information
|
||||
> about the customer they're currently helping. Even if multiple agents are helping customers
|
||||
> simultaneously, each agent's notes are completely separate from the others'. When an agent finishes
|
||||
> with one customer and moves to the next, they start with a fresh notepad. This approach eliminates
|
||||
> the need for agents to coordinate their note-taking, as each agent's notes are private to their
|
||||
> current customer interaction.
|
||||
|
||||
### In plain words
|
||||
|
||||
> Thread-Specific Storage provides each thread with its own private copy of data, isolating thread
|
||||
> interactions and avoiding the need for synchronization mechanisms.
|
||||
|
||||
### Wikipedia says
|
||||
|
||||
> Thread-local storage (TLS) is a computer programming method that uses static or global memory
|
||||
> local to a thread. TLS is a common technique for avoiding race conditions when multiple threads
|
||||
> need to access the same data. Each thread has its own copy of the data, so there is no need for
|
||||
> synchronization.
|
||||
|
||||
### Class diagram
|
||||
|
||||

|
||||
|
||||
## Programmatic Example of Thread-Specific Storage Pattern in Java
|
||||
|
||||
Imagine a web application that needs to track the current user's context across different stages of request processing.
|
||||
|
||||
Each request is handled by a separate thread, and we need to maintain user-specific information without
|
||||
synchronization overhead.
|
||||
|
||||
The Thread-Specific Storage pattern efficiently handles this by providing each thread with its own copy
|
||||
of the user context data.
|
||||
|
||||
```java
|
||||
@Slf4j
|
||||
public class APP {
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here's how the request handler processes each request:
|
||||
|
||||
```java
|
||||
@Slf4j
|
||||
public class RequestHandler {
|
||||
private final String token;
|
||||
|
||||
public RequestHandler(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
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();
|
||||
Random random = new Random();
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// ... parseToken method implementation
|
||||
}
|
||||
```
|
||||
|
||||
The UserContextProxy acts as a thread-safe accessor to the ThreadLocal storage:
|
||||
|
||||
```java
|
||||
public class UserContextProxy {
|
||||
// Underlying TSObjectCollection (ThreadLocalMap) managed by JVM.
|
||||
// This ThreadLocal acts as the Key for the map.
|
||||
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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here's a sample console output:
|
||||
|
||||
```
|
||||
Start handling request with token: token::1
|
||||
Start handling request with token: token::2
|
||||
Start handling request with token: token::3
|
||||
Start handling request with token: token::1
|
||||
1's account: 234
|
||||
Start handling request with token: token::2
|
||||
2's account: 157
|
||||
3's account: 89
|
||||
1's account: 342
|
||||
2's account: 76
|
||||
```
|
||||
|
||||
**Note:** Since this example demonstrates concurrent thread execution, **the actual output may vary between runs**. The order of execution and timing can differ due to thread scheduling, system load, and other factors that affect concurrent processing. However, each thread will correctly maintain its own user context without interference from other threads.
|
||||
|
||||
## When to Use the Thread-Specific Storage Pattern in Java
|
||||
|
||||
* When you need to maintain per-thread state without synchronization overhead
|
||||
* For applications that process requests in multiple stages and need to share data across those stages
|
||||
* To avoid concurrency issues when working with non-thread-safe objects like SimpleDateFormat
|
||||
* When implementing logging or security context that needs to be accessible throughout a request processing
|
||||
* To maintain thread-specific caches or counters without risk of data corruption
|
||||
|
||||
## Thread-Specific Storage Pattern Java Tutorial
|
||||
|
||||
* [Thread-Specific Storage Pattern Tutorial (Baeldung)](https://www.baeldung.com/java-threadlocal)
|
||||
|
||||
## Real-World Applications of Thread-Specific Storage Pattern in Java
|
||||
|
||||
* Servlet containers use ThreadLocal to maintain the current request and response objects
|
||||
* Spring Framework uses ThreadLocal for managing transaction contexts and security contexts
|
||||
* Logging frameworks use ThreadLocal to associate log messages with the current thread's context
|
||||
* Database connection management where each thread needs its own connection or maintains a connection pool per thread
|
||||
* User session management in web applications where session data is accessed across multiple layers
|
||||
|
||||
## Benefits and Trade-offs of Thread-Specific Storage Pattern
|
||||
|
||||
### Benefits
|
||||
|
||||
* Eliminates the need for synchronization mechanisms, improving performance
|
||||
* Provides complete isolation of data between threads, preventing concurrency issues
|
||||
* Simplifies code by removing the need to pass context objects through method parameters
|
||||
* Enables safe use of non-thread-safe classes in multi-threaded environments
|
||||
* Reduces object creation overhead by reusing thread-local instances
|
||||
|
||||
### Trade-offs
|
||||
|
||||
* Can lead to increased memory consumption as each thread maintains its own copy of data
|
||||
* Requires careful cleanup to prevent memory leaks, especially in long-running applications
|
||||
* May complicate debugging as data is not visible across threads
|
||||
* Can cause issues in environments with thread pooling where threads are reused (data from previous tasks may persist)
|
||||
|
||||
## Related Java Design Patterns
|
||||
|
||||
* [Context Object Pattern](https://java-design-patterns.com/patterns/context-object/): Encapsulates
|
||||
request-specific information into a context object that can be passed between components.
|
||||
* [Thread Pool Pattern](https://java-design-patterns.com/patterns/thread-pool-executor/):
|
||||
Maintains a pool of worker threads to execute tasks concurrently, optimizing resource usage.
|
||||
* [Singleton Pattern](https://java-design-patterns.com/patterns/singleton/): Ensures a class has only
|
||||
one instance and provides global access to it, similar to how ThreadLocal provides per-thread access.
|
||||
|
||||
## References and Credits
|
||||
|
||||
* [Pattern-Oriented Software Architecture, Volume 2: Patterns for Concurrent and Networked Objects](https://www.amazon.com/Pattern-Oriented-Software-Architecture-Concurrent-Networked/dp/0471606952)
|
||||
* [Java Documentation for ThreadLocal](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ThreadLocal.html)
|
||||
* [Java Concurrency in Practice](https://jcip.net/) by Brian Goetz
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -0,0 +1,41 @@
|
||||
@startuml
|
||||
package thread-specific-storage {
|
||||
|
||||
class APP {
|
||||
+ {static} main(args : String[]) : void
|
||||
}
|
||||
|
||||
class Thread {
|
||||
+ start() : void
|
||||
}
|
||||
|
||||
class UserContext {
|
||||
- userId : Long
|
||||
+ UserContext(userId : Long)
|
||||
+ getUserId() : Long
|
||||
+ setUserId(userId : Long) : void
|
||||
}
|
||||
|
||||
class UserContextProxy {
|
||||
- {static} userContextHolder : ThreadLocal<UserContext>
|
||||
+ {static} set(context : UserContext) : void
|
||||
+ {static} get() : UserContext
|
||||
+ {static} clear() : void
|
||||
}
|
||||
|
||||
class RequestHandler {
|
||||
- token : String
|
||||
+ RequestHandler(token : String)
|
||||
+ process() : void
|
||||
- parseToken(token : String) : Long
|
||||
}
|
||||
|
||||
UserContextProxy --> UserContext : manages
|
||||
RequestHandler --> UserContextProxy : uses static
|
||||
RequestHandler --> UserContext : creates
|
||||
APP --> RequestHandler : creates
|
||||
APP --> Thread : starts
|
||||
Thread --> RequestHandler : executes
|
||||
|
||||
}
|
||||
@enduml
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
|
||||
The MIT License
|
||||
Copyright © 2014-2022 Ilkka Seppälä
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>java-design-patterns</artifactId>
|
||||
<version>1.26.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>thread-specific-storage</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.awaitility</groupId>
|
||||
<artifactId>awaitility</artifactId>
|
||||
<version>4.2.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>com.iluwatar.threadspecificstorage.App</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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